Showing posts with label job. Show all posts
Showing posts with label job. Show all posts

Saturday, July 24, 2021

A bad job with a very good description

Today I received a job invitation for a C++ developer position. My C++ skills are not up to date, so I initially doubted whether to apply. Nevertheless, I clicked a link with a more detailed job description. And this was the best job description that I have seen so far. No long list of desired skills. Clear view of the job duties.

Let me translate and quote the relevant parts:

Our product is <link removed>. You can download and try it on your PC.

Well, this is why I refused. I do not want to be involved in "black SEO" (which is obvious from the link that I removed). But let's continue with the description:

Our mission is to allow users to create separate browser instances, with their own cookies, local storage, etc. And with their own substitutions of values such as navigator.platform, navigator.language and so on.

For these tasks, we use currently a stock Google Chrome browser, and embed the needed property replacements using its API (Devtools Protocol) as JavaScript code. But this approach has certain drawbacks that do not allow bringing the browser operation to the desired "perfect" state. Therefore, we start development of our own browser build based on Chromium sources, so that all the needed property substitutions and modifications would come from the core and not from the injected JavaScript code.

A perfectly clear  and logical description of what they are trying to achieve, and, importantly, why. It is obvious that the person who wrote this has a coherent product vision.

Let me snip the part regarding work conditions, as that's irrelevant for the purpose of this blog post, and continue with the best part of the job post:

And here is a benchmark that allows you to see if you fit this role. Create a Chromium build for Windows with a replaced icon. Just any icon, but make sure that it shows up both as an icon of the executable and in the taskbar. Also, modify the browser so that the navigator.platform property is always MacIntel.

This benchmark allows candidates to self-judge themselves much better than long lists of required skills. What if I don't have this one skill from the list - is it really important, or can I just learn it on the fly? With the candidate self-test approach, as demonstrated above, such question cannot arise, and there is less need to conduct in-person coding interviews.

Another good aspect of this benchmark is that it involves modification of an existing code base, and not coding something from scratch. The ability to read and understand existing code in real-world projects is what really matters, and traditional coding interviews do not measure that.

I wish all IT job descriptions were written like that. It's unfortunate that this position comes from the bad guys - otherwise I would have definitely applied.

P.S. If you need a remote Linux sysadmin, DevOps engineer, or a C, Python, or PHP developer, feel free to contact me via email.

Thursday, May 3, 2018

Downtime

A few days ago I had an interesting case of a server downtime. The server is just a playground for developers, so no big deal. But still, lessons learned.

The reports came almost simultaneously from developers and from the monitoring system, "cannot connect". And indeed, the server was not pingable. Someone else's server, with IP equal to the IP of our server with the last octet increased by 2, was pingable, so I concluded it was not a network problem.

Next reaction: look at the server's screen, using remote KVM provided by the hoster. Kernel panic! OK, need to screenshot it (done) and reboot the server. Except that the Power Control submenu in the viewer is grayed out, so I can't. And a few months ago, when we needed a similar kind of reset, it was there.

OK, so I created a ticket for resetting the server manually. And I had to remind them that the remote reboot functionality is supposed to work. Here is the hoster's reply (PDU = power distribution unit):

Dear Alexander,

Upon checking on the PDU, the PDU is refusing connection.

We'll arrange a PDU replacement the soonest possible.

We apologise for the inconvenience caused.

Everybody reading this post, now, please check that you don't fall into the same trap. Run your iKVM viewer against each of your server that it can connect to, and check that it runs, and that the menu item to reset the server still exists. Create a calendar reminder to periodically recheck it.

And maybe append "panic=10" to your linux kernel command line, so that manual intervention is not needed next time.

Sunday, October 18, 2015

Still using icims.com for recruiting? Think again!

If your company has open vacancies and uses some system for pre-screening candidates (e.g. by giving them questions), I have a "small" task for you. Go to your system, answer the questions as if you were a candidate, validate the answers as you would expect from a candidate (e.g. actually perform the actions that the answer describes), and then save the results. Look at the whole process. Make a conclusion for yourself whether your system is usable for the stated purpose. Communicate it to your management, if needed.

If you are using icims.com for hiring technical candidates, the answer is most probably "not suitable at all".

The most annoying bug that icims.com has is that it does not allow the candidate to enter certain characters in certain positions. The exact error message is:
Q3 2 Contains invalid characters. You cannot use the characters: ' " \ / or ` in an enclosing instance of <>, <<, >> or ><.
 This triggers at least on the following types of input:
  • XML or HTML
  • Command redirections, e.g.: echo "foo bar" >> baz.txt
  • Sequences of menu items to click, e.g.: "File > New > Folder", if a bad character happens to be before that
So, you cannot ask questions about HTML, shell scripting, or even general questions about using GUI-based applications.

This error message probably means that they are concerned about XSS attacks. However, filtering out invalid characters is a very sloppy way of protection against such attacks. And it imposes completely unreasonable restrictions on the user input.

In fact, any kind of input (including XML, bash scripts or text about clicking the menu) should be suitable, and can be made to display safely and properly in any browser, just by escaping the special characters when generating the HTML page. Many template engines exist that do this escaping for you automatically. Today, there is simply no reason not to use them.

If a candidate sees such error, he/she becomes demotivated. It is a stupid barrier before getting the correct answer to you. It also indicates that you don't care about your customers (by choosing business partners that allow such sloppy practices). Worse, some of your candidates (who see icims.com for the first time) can think that it is your product, or your internal system, and that you (not icims.com) have web developers with insufficient skills. I.e. that your company is not good enough to work in, because you don't weed out underqualified workers.

You don't want to lose candidates. So you don't want to use icims.com. Really.

Monday, August 27, 2012

SafeDNS.com is now open for registration

The company I work for decided to establish its presence in USA. So, please welcome a new player in the international market of content filtering solutions: SafeDNS. Strictly speaking, we are still in beta and need YOU to help us kill the last bugs.

This service can be useful if you want to protect yourself or your kids against accidentally opening sites with unsuitable content. Or, to prevent your employees from wasting time at work on such things as social networking and videos. Or even to evade a bad filter set up by your ISP :)

We have more than 4 mln sites sorted into more than 50 categories, and it's you who decides what to block and what to let through. All you need is an e-mail address, a public static IPv4 address on your router and the ability to change DNS settings on your computers.

Register now (it's free!), read the guide, and help us improve the service by sending feedback.

Friday, November 25, 2011

Namespace issues in Apache

Some time ago I was given a task to write a Python WSGI script that handles all (really all!) URLs on a given Apache-based virtual host. Doesn't that sound easy? Here is the "obvious" part of the Apache configuration file for that virtual host:

WSGIScriptAlias / /path/to/script.wsgi

Except that it is slightly wrong. The problem is that the script really should handle all URLS, even "evil" ones, without any omissions. In the above form, it doesn't.

First, if the URL contains a percent-encoded slash (e.g., as in http://example.org/foo%2fbar), Apache gives a 404 error by default, without even calling the script. Solution:

AllowEncodedSlashes On

Wait, there is more! Regular aliases have higher priority than our WSGIScriptAlias, and that there are some default aliases usually set by distributions. Due to that, http://example.org/icons/folder.gif will map to a static file, not to the WSGI script. There is a wishlist bug in Apache that one cannot easily remove aliases from the namespace.

The solution (or, more precisely, a bad hack) that I found is to use mod_rewrite. By adding some prefix to all URLs (and, of course, dealing with it in the script), one makes sure that the existing aliases are not hit:

AllowEncodedSlashes On

RewriteEngine On
RewriteRule ^/(.*)$ /dummyprefix/$1 [PT]

WSGIScriptAlias /dummyprefix /path/to/script.wsgi

Still, this doesn't work (gives a 404 error instead of calling the script) on URLs that include some bad characters such as newlines, e.g. http://example.org/foo%0abar . Removing the dollar sign from the pattern fixes the 404 error, calls the script, but it then receives a truncated PATH_INFO in the environment. Still, SCRIPT_URI is correct, so this may be enough. In fact, if the script doesn't care about PATH_INFO, even this works:

AllowEncodedSlashes On

RewriteEngine On
RewriteRule ^/ /dummyprefix [PT]

WSGIScriptAlias /dummyprefix /path/to/script.wsgi

The real cause of the issue with the original RewriteRule is that the "." metacharacter doesn't really match all characters. Indeed, it doesn't match a newline. So, in order to match the full URL, including the evil encoded newline in the middle, one has to write an explicit range covering all possible characters:

AllowEncodedSlashes On

RewriteEngine On
RewriteRule ^/([\x00-\xff]*)$ /dummyprefix/$1 [PT]

WSGIScriptAlias /dummyprefix /path/to/script.wsgi

Whoops. Now a typical sysadmin probably won't understand the need (or will forget the reason) behind such a complex configuration for a seemingly simple issue of passing all URLs to a single script. And I am still not 100% sure that all valid URLs are really handled by the script. Maybe I should have started with a different web server, the one that doesn't have a polluted namespace in virtual hosts by default.

Update: In the comments, the following was suggested:

AllowEncodedSlashes On
WSGIHandlerScript wsgi-handler /path/to/handler.wsgi
SetHandler wsgi-handler

Here handler.wsgi would be a typical WSGI script, except that it provides the "handle_request" callable object instead of "application".

This solution looks elegant and simple, but, if PHP is also installed on the same server, it is wrong (thus proving the complexity of the problem and the fragility of Apache URL namespace). It misses URLs that end in .php (even though the document root s empty). So, back to the ugly solution based on mod_rewrite.

Friday, November 12, 2010

Minimizing server downtime

Suppose that you are responsible for a typical LAMP server running Debian. The goal is to have the needed security updates and minimize downtime. Unfortunately, due to installation of security updates, services need to be restarted, and sometimes (after kernel updates) even a reboot is required. Here are some tips that will help you shrink the time of service unavailability.

  1. Upgrade your key services (apache, php and mysql) separately from the rest of software. Reason: if you just run "apt-get dist-upgrade" to upgrade everything, then your MySQL server will be stopped at the beginning of the whole upgrade and started only at the end, after unpacking all other packages. So, run "apt-get install mysql-server" before "apt-get dist-upgrade" to minimize the delay.
  2. If you know that kexec works on your hardware, use it: apt-get install kexec-tools. This way, you will avoid the long time (sometimes more than a minute) while your server shows useless BIOS screens. Instead of rebooting to the BIOS, the old kernel will load the new kernel directly and pass control to it, thus avoiding the delay.

Monday, May 10, 2010

New job

This blog didn't receive any updates for quite a while. That's both because I was too busy at Yandex and because there were no interesting bugs to blog about. I no longer work at Yandex. There was no single big reason for this decision, just a lot of small issues that accumulated and that don't apply to anyone else.

Now I work for a different company (won't name it here, it is nether SEO nor Google) as a senior developer of a web service. The most pleasant result of the job change is that now I feel more like a developer: write mostly C code, not bash scripts, and can even make architectural decisions.

Sunday, September 6, 2009

Matching multiple strings

Sometimes, it is necessary to check efficiently which of the many input strings contain one of the few pre-defined substrings.

The simplest approach



This task, of course, can be solved inefficiently by brute force, i.e. by passing each substring to strstr(), as demonstrated in the following benchmark that tries to search for known names of linux games in the same string (that actually doesn't match) 100000 times:


/* compile as follows: gcc -o benchmark1 benchmark1.c */
#include <stdio.h>
#include <string.h>
#include <sys/time.h>

const char* words[] = { "actioncube", "alien arena", "astromenace",
"armagetron", "assault cube", "atomorun2008", "battle for wesnoth",
"blob wars", "breakout", "briquolo", "bzflag", "celetania",
"conquest", "enigma", "freeciv", "freecol", "freeorion",
"freetrain", "glest", "hedgewars", "heretic", "hexen",
"jardinanis", "lordsawar", "lxdream", "maniadrive", "neverball",
"nexuiz", "oolite", "openarena", "opencity", "openmw",
"open quartz", "openttd", "paintball 2", "planeshift", "postal 2",
"sacred", "scorched 3d", "smokin' guns", "supertux", "supertuxkart",
"teeworlds", "tremulous", "urban terror", "vegastrike", "warsow",
"wormux", "x-moto" };

const char* text = "gnome uses gconf to store all of its configuration";

int check(const char* p)
{
int i;
for (i = 0; i < sizeof(words)/sizeof(words[0]); i++) {
if (strstr(p, words[i]) != NULL)
return 0;
}
return -1;
}

int main(int argc, char* argv[])
{
struct timeval start, end;
int usec_spent;
int i;
int result;

gettimeofday(&start, NULL);
for (i = 0; i < 100000; i++)
result = check(text);

gettimeofday(&end, NULL);
if (result == 0)
printf("The string matched\n");
else
printf("The string didn't match\n");
usec_spent = 1000000 * (end.tv_sec - start.tv_sec);
usec_spent += (end.tv_usec - start.tv_usec);
printf("Spent %d ms\n", usec_spent / 1000);
return 0;
}


On my desktop, this benchmark takes 2925 ms.

Aho-Corasick algorithm



Special fast algorithms exist for the same task. All of them preprocess the given set of substrings and use the result of such preprocessing when deciding whether the input string matches. One of such ahorithms is the Aho-Corasick algorithm. A free (BSD-licensed) implementation is available for download. Unfortunately, this implementation operates on wide-character strings and thus is not directly comparable with the strstr() approach. Also, if one of the substrings matches the very beginning of the input string, one cannot distinguish this situation with CSuffixTrie::SearchAhoCorasik() from non-match by looking only atCSuffixTrie::_DataFound::iFoundPosition. To compensate for this, I have extracted the SuffixTrie.{h,cpp} files from the archive and applied the following edits:


--- SuffixTrie.h
+++ SuffixTrie.h
@@ -51,7 +51,7 @@
{
public:
//Our string type
- typedef std::wstring SearchString;
+ typedef std::string SearchString;

//Data returned from our search
typedef struct _DataFound
@@ -107,7 +107,7 @@
virtual ~CSuffixTrie();
private:
//Our char search type
- typedef wchar_t SearchChar;
+ typedef char SearchChar;

//Forward declare the node
struct _Node;
--- SuffixTrie.cpp 2008-09-24 02:20:46.000000000 +0600
+++ SuffixTrie.cpp 2009-09-06 15:22:11.000000000 +0600
@@ -1,4 +1,3 @@
-#include "stdafx.h"
#include "SuffixTrie.h"

CSuffixTrie::CSuffixTrie()
@@ -136,7 +135,7 @@
void CSuffixTrie::BuildTreeIndex()
{
//Build index on the root
- BuildIndex(L"",
+ BuildIndex("",
&m_aRoot);
}

@@ -261,7 +260,7 @@
{
//Our data found
DataFound aData;
- aData.iFoundPosition=0;
+ aData.iFoundPosition=-1;

//The current string we match
SearchString sMatchedString;
@@ -295,7 +294,7 @@
pNode=&m_aRoot;

//Reset search string
- sMatchedString=L"";
+ sMatchedString="";

//Did we do a switch?
if (bSwitch)
@@ -386,7 +385,7 @@
pNode=&m_aRoot;

//Reset search string
- sMatchedString=L"";
+ sMatchedString="";

//Did we do a switch?
if (bSwitch)
@@ -439,7 +438,7 @@
iCount-=sMatchedString.length()-1;

//Reset the data
- sMatchedString=L"";
+ sMatchedString="";
}
}

@@ -495,7 +494,7 @@

//Start to build the trie
BuildStrings(aVector,
- L"",
+ "",
&m_aRoot);

//Done


Here is a benchmark:


/* compile as follows: g++ -O2 -o benchmark2 benchmark2.cpp SuffixTrie.cpp */
#include "SuffixTrie.h"
#include <stdio.h>
#include <string>
#include <sys/time.h>

const char* words[] = { "actioncube", "alien arena", "astromenace",
"armagetron", "assault cube", "atomorun2008", "battle for wesnoth",
"blob wars", "breakout", "briquolo", "bzflag", "celetania",
"conquest", "enigma", "freeciv", "freecol", "freeorion",
"freetrain", "glest", "hedgewars", "heretic", "hexen",
"jardinanis", "lordsawar", "lxdream", "maniadrive", "neverball",
"nexuiz", "oolite", "openarena", "opencity", "openmw",
"open quartz", "openttd", "paintball 2", "planeshift", "postal 2",
"sacred", "scorched 3d", "smokin' guns", "supertux", "supertuxkart",
"teeworlds", "tremulous", "urban terror", "vegastrike", "warsow",
"wormux", "x-moto" };

const char* text = "gnome uses gconf to store all of its configuration";

int main(int argc, char* argv[])
{
struct timeval start, end;
int usec_spent;

CSuffixTrie aTree;
int i;
for (i = 0; i < sizeof(words)/sizeof(words[0]); i++)
aTree.AddString(words[i]);

aTree.BuildTreeIndex();

std::string stext = text;
int pos;

gettimeofday(&start, NULL);
for (i = 0; i < 100000; i++) {
pos = aTree.SearchAhoCorasik(stext).iFoundPosition;
}
gettimeofday(&end, NULL);

if (pos == -1)
printf("The string didn't match\n");
else
printf("The string matched\n");

usec_spent = 1000000 * (end.tv_sec - start.tv_sec);
usec_spent += (end.tv_usec - start.tv_usec);
printf("Spent %d ms\n", usec_spent / 1000);
return 0;
}



On my desktop, this benchmark takes 326 ms. I.e., this is indeed faster than the simple strstr()-based approach.

Regular expressions



One may wonder why the Aho-Corasick algorithm is not available in the standard C library. The answer is that a more general mechanism, based on a similar idea, is available: regular expressions. One first constructs a regular expression by escaping and joining the given substrings using "|" as the separator, then compiles this regular expression with regcomp() and uses the result in regexec(). Here is a benchmark:


/* compile as follows: gcc -o benchmark3 benchmark3.c */
#include <stdio.h>
#include <regex.h>
#include <string.h>
#include <sys/time.h>

const char* re = "(actioncube|alien arena|astromenace|armagetron|"
"assault cube|atomorun2008|battle for wesnoth|blob wars|breakout|"
"briquolo|bzflag|celetania|conquest|enigma|freeciv|freecol|"
"freeorion|freetrain|glest|hedgewars|heretic|hexen|jardinanis|"
"lordsawar|lxdream|maniadrive|neverball|nexuiz|oolite|openarena|"
"opencity|openmw|open quartz|openttd|paintball 2|planeshift|"
"postal 2|sacred|scorched 3d|smokin' guns|supertux|supertuxkart|"
"teeworlds|tremulous|urban terror|vegastrike|warsow|wormux|x-moto)";

const char* text = "gnome uses gconf to store all of its configuration";

int main(int argc, char* argv[])
{
struct timeval start, end;
int usec_spent;
int i;
regex_t reg;
int result;

result = regcomp(&reg, re, REG_EXTENDED | REG_NOSUB);
if (result)
return 1;

gettimeofday(&start, NULL);
for (i = 0; i < 100000; i++)
result = regexec(&reg, text, 0, NULL, 0);
gettimeofday(&end, NULL);
if (result == 0)
printf("The string matched\n");
else
printf("The string didn't match\n");
usec_spent = 1000000 * (end.tv_sec - start.tv_sec);
usec_spent += (end.tv_usec - start.tv_usec);
printf("Spent %d ms\n", usec_spent / 1000);
return 0;
}


On this linux x86 system with glibc-2.10.1, it takes 312 ms, i.e., as good as the Aho-Corasick implementation referenced above.

On other platforms, the result may be different, because the system C library uses a different regular expression engine. E.g., the same benchmark, when compiled with TRE (replace <regex.h> with <tre/regex.h> and add -ltre to the gcc command line), takes 4158 ms, i.e., more than the brute-force approach. Since NetBSD will use TRE in the next release, this unsatisfactory result will likely manifest itself there. So, if you want to give the same performance guarantees on all platforms, don't use regcomp()/regexec().

The same note about unsatisfactory speed (8884 ms here) applies to PCRE. To repeat this test, replace <regex.h> with <pcreposix.h> and add -lpcreposix to gcc command line.

Generated code



It is possible to match multiple substrings much faster, if they are known at compile time. The trick is to convert the regular expression to C code with a tool such as re2c. Here is a benchmark of this approach:


/* compile as follows:
re2c benchmark4.re >benchmark4.c
gcc -O1 -o benchmark4 benchmark4.c */
#include <stdio.h>
#include <sys/time.h>

int check(const char* p)
{
const unsigned char* marker;
/*!re2c
re2c:define:YYCTYPE = "unsigned char";
re2c:define:YYCURSOR = p;
re2c:yyfill:enable = 0;
re2c:define:YYMARKER = marker;
re2c:yych:conversion = 1;
[\001-\377]*( "actioncube" | "alien arena" |
"astromenace" | "armagetron" |
"assault cube" | "atomorun2008" |
"battle for wesnoth" | "blob wars" |
"breakout" | "briquolo" | "bzflag" |
"celetania" | "conquest" | "enigma" |
"freeciv" | "freecol" | "freeorion" |
"freetrain" | "glest" | "hedgewars" |
"heretic" | "hexen" | "jardinanis" |
"lordsawar" | "lxdream" | "maniadrive" |
"neverball" | "nexuiz" | "oolite" |
"openarena" | "opencity" | "openmw" |
"open quartz" | "openttd" | "paintball 2" |
"planeshift" | "postal 2" | "sacred" |
"scorched 3d" | "smokin' guns" |
"supertux" | "supertuxkart" |
"teeworlds" | "tremulous" |
"urban terror" | "vegastrike" |
"warsow" | "wormux" |
"x-moto" ) { return 0; }
"" { return 1; }
*/
}

const char* text = "gnome uses gconf to store all of its configuration";

int main(int argc, char* argv[])
{
struct timeval start, end;
int usec_spent;
int i;
int result;

gettimeofday(&start, NULL);
for (i = 0; i < 100000; i++)
result = check(text);
gettimeofday(&end, NULL);
if (result == 0)
printf("The string matched\n");
else
printf("The string didn't match\n");
usec_spent = 1000000 * (end.tv_sec - start.tv_sec);
usec_spent += (end.tv_usec - start.tv_usec);
printf("Spent %d ms\n", usec_spent / 1000);
return 0;
}


When compiled with -O1 or -O3, this benchmark runs 12 ms here (i.e., almost 250 times faster than the brute-force approach). However, the -O2 optimization option makes the code slower (it runs 16 ms).

Tuesday, January 6, 2009

Inefficient use of Valgrind

This is already stated in the documentation, but let me repeat. Valgrind can detect memory leaks only if the application uses malloc() and free(), or, in C++, operator new() and operator delete().

This is not always the case. E.g., ImageMagick, when configured with the --enable-embeddable switch, uses mmap() directly in order to allocate memory for its needs. Other problematic cases involve custom memory pools on top of malloc() and free(), as done in Glib.

So, if you want to find a memory leak with Valgrind, the first thing to do is to turn off all those custom memory allocation schemes. OTOH, if the buggy thing is the allocator itself, this will hide the bug.

Saturday, December 13, 2008

JPEG quality is a meaningless number

Suppose that a web designer tells you:

Your task is to make a script that creates images for this web page. The images should be in JPEG format, have such-and-such dimensions, and their quality should be this number of percent.


Ignore the bit about the desired quality, especially if the web designer uses Adobe Photoshop and has tested his page only with images made with Photoshop. The reason is that the same number yields different quality in different applications. Let me demonstrate this using this original image.

The image has been resized to 400x300 pixels using Photoshop (that contains Adobe's own JPEG encoder) and GIMP (that uses libjpeg from IJG, as many web scripts do). Here are the screenshots of the corresponding settings.

Photoshop:
Photoshop setup

GIMP:
GIMP setup

As you see, both programs are set to optimization of Huffman tables, no smoothing, no chroma subsampling, and 60% quality according to their scales. Here are the resulting images.

Photoshop:
Parrots, compressed with Adobe Photoshop

GIMP:
Parrots, compressed with GIMP

As you can see, the image created by Adobe Photoshop is almost perfect, while on the image created with GIMP there are visible artifacts in the form of double contours around the parrots in the top row. This is not what the web designer wanted.

Here are some statistics. The image made with Photoshop takes 46 KB, while the image made with GIMP takes only 26 KB.

The quantization tables stored in the JPEG files are also different. If you don't know how to interpret numbers in them: the bigger the number is, the more precision is lost. The closer the number is to the right or bottom, to finer horizontal or vertical details it corresponds.

Photoshop:

Luminance Chrominance
6 4 4 6 9 11 12 16 7 7 13 24 26 31 31 31
4 5 5 6 8 10 12 12 7 12 16 21 31 31 31 31
4 5 5 6 10 12 14 19 13 16 17 31 31 31 31 31
6 6 6 11 12 15 19 28 24 21 31 31 31 31 31 31
9 8 10 12 16 20 27 31 26 31 31 31 31 31 31 31
11 10 12 15 20 27 31 31 31 31 31 31 31 31 31 31
12 12 14 19 27 31 31 31 31 31 31 31 31 31 31 31
16 12 19 28 31 31 31 31 31 31 31 31 31 31 31 31


GIMP:

Luminance Chrominance
13 9 8 13 19 32 41 49 14 14 19 38 79 79 79 79
10 10 11 15 21 46 48 44 14 17 21 53 79 79 79 79
11 10 13 19 32 46 55 45 19 21 45 79 79 79 79 79
11 14 18 23 41 70 64 50 38 53 79 79 79 79 79 79
14 18 30 45 54 87 82 62 79 79 79 79 79 79 79 79
19 28 44 51 65 83 90 74 79 79 79 79 79 79 79 79
39 51 62 70 82 97 96 81 79 79 79 79 79 79 79 79
58 74 76 78 90 80 82 79 79 79 79 79 79 79 79 79


So indeed, Photoshop saves more details than GIMP at the indicated settings. The meaningless quality percentages match, the actual quality settings don't. In order to achieve in GIMP (and thus in web scripts that use the same libjpeg library from IJG) what the web designer actually meant, one would need to use quality=85% or so.

Sunday, November 16, 2008

Strange backtrace

Some time ago I had to debug a strange crash. It was in a multithreaded program and manifested itself only on FreeBSD i386. The code (with all the needed declarations included, seemingly irrelevant details removed, and everything renamed) looks like this:


#include <cstdio>
#include <cstring>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
#include <exception>


class system_error : public exception
{
public:
system_error() throw() :error_text(strerror(errno)) {}
virtual const char* what() const throw() { return error_text; }
private:
const char* error_text;
};

class strange_thing
{
public:
strange_thing(); // fills in some useful defaults
private:
// lots of implementation details
};

class strange_container
{
public:
strange_container();
~strange_container() { if (fd != -1) close(fd); }
void play_with_strange_thing(const char* filename);
private:
int fd;
};

strange_container::strange_container()
: fd(-1)
{
play_with_strange_thing("test.file");
}

void strange_container::play_with_strange_thing(const char* filename)
{
fd = open(filename, O_CREAT | O_TRUNC | O_RDWR, 0777);
if (fd == -1)
throw system_error();
strange_thing ss;
/* here goes some code that uses ss and fd */
}

// well, actually it is not the main function, but something buried in a thread
int main(int argc, char* argv[])
{
strange_container c;
return 0;
}



The test file is not created, and the segfault looks like this:

[aep@bsd1 ~/crashtest]$ gdb ./a.out
GNU gdb 6.1.1 [FreeBSD]
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-marcel-freebsd"...
(gdb) run
Starting program: /usr/home/aep/crashtest/a.out
[New LWP 100043]
[New Thread 0x28301100 (LWP 100043)]

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x28301100 (LWP 100043)]
strange_container::play_with_strange_thing (this=0x28306098,
filename=0x8048c33 "test.file") at crashtest.cpp:57
57 fd = open(filename, O_CREAT | O_TRUNC | O_RDWR, 0666);


At this point, everything looks valid, including the "this" pointer. By all applicable logic, the program just cannot segfault by calling open() with valid parameters. So I started adding debugging printf() statements. The statement just before the call to play_with_strange_thing() worked fine, and none of the statements inside play_with_strange_thing() worked. Moreover, when I added a printf() as the very first line of strange_container::play_with_strange_thing() and ran gdb on the result, it showed this printf() in the backtrace!

So, I didn't believe my eyes. I thought (wrongly) that printf() and buffering somehow interacts with the segfault, and thus invented a different mechanism to find out whether a certain line of code was reached by the program. Namely, I replaced all my debugging printf() calls in strange_container::play_with_strange_thing() with throwing exceptions, with the intention to remove them one-by-one:


void strange_container::play_with_strange_thing(const char* filename)
{
throw system_error(); // (1)
fd = open(filename, O_CREAT | O_TRUNC | O_RDWR, 0777);
throw system_error();
if (fd == -1)
throw system_error();
throw system_error(); // (2)
strange_thing ss;
throw system_error(); // (3)
/* here goes some code that uses ss and fd, err... throws system_error() */
}


This worked. I knew for sure that point (2) was reached, and point (3) was't. So there is something bad with creation of the strange thing that, however, doesn't cause gdb to complain about its constructor.

So, I had to take another look into the implementation of the strange_thing class. The issue was actually with the huge size of the object (several megabytes)! So, no wonder that it overflowed the thread stack. You can reproduce the crash on your own system by replacing "lots of implementation details" with "char c[2000000];", implementing the strange_thing default constructor, and running with a low-enough "ulimit -s" setting.

As the reason of the crash became known, it was an easy matter to fix properly, by not creating huge objects on the stack.

Thursday, September 18, 2008

Recording SIP conversations

My chief prefers to use SIP (voice conversations) instead of Jabber (text messaging). The problem is that he sometimes overloads me with a lot of information, and later I forget 90%. This is not a problem with Jabber, as there are logs. However, I don't know any SIP client that supports recording of conversations out of the box.

Here is my solution. You need alsa-lib 1.0.17 or later (i.e., if you are using Debian Lenny, due to the freeze, you'll have to fetch libasound2 and libasound2-dev from experimental). Create the subdirectory named "voice" in your home directory. Add these lines to $HOME/.asoundrc:

pcm.voice {
type asym
playback.pcm {
type file
file "/home/user/voice/p"
slave.pcm "default"
format "wav"
truncate 0
}
capture.pcm {
type file
file "/home/user/voice/c"
slave.pcm "default"
format "wav"
truncate 0
}
}


Then, configure your SIP client (I use twinkle) as follows. ALSA device for rings: "default", ALSA speaker: "voice", ALSA microphone: "voice", Play back ring tone when the network doesn't: disable. As the result, ALSA library will write the following files into the voice directory: p, c, p.0001, c.0001, p.0002, c.0002, and so on (the number is incremented for each new call). All these files are in wav format, "p" files correspond to the remote side, and "c" files contain what you spoke into the microphone. It appears that it is impossible to record both parties of the conversation into one file using only built-in features that come with ALSA.

To play back the conversation, do:

aplay p.0002 & aplay c.0002


This will cause two aplay processes to be started in parallel, with their outputs mixed. Or, you can make one mixed wav file with sox:

sox -m c.0002 p.0002 -f wav talk0002.wav

Tuesday, September 16, 2008

New job!


Office building at night

Since September 15, 2008, I work at Yandex, as a developer in the Image search department. I am busy with bash scripts, C++ and even XML. My work computer is a Fujitsu Siemens S laptop, and I installed Debian Lenny there. One strange thing about this job is that I have never seen my chief: he is in Moscow, and all conversations with him are via Jabber and SIP.

The office in Yekaterinburg is clean, there are nice flowers in front of the building, and my colleagues seem to enjoy working there. Our windows (on the fifth floor) remain lit even at 10 PM.