Re: BSDiff for cache objects

Bogdan Harjoc <[email protected]>
Newsgroups gmane.comp.compilers.ccache
Message-ID <CAF4+tmof0Cq1KoURRBRRqd3-QxUD4-AezCeB7AT3fO33oA+HtA@mail.gmail.com>
On Mon, Nov 12, 2012 at 8:09 PM, Bogdan Harjoc <[email protected]> wrote:

> Initial results from a small .ccache (3.0) dir:
>

The previous results were bogus (I was diffing .gz compressed objects).

I tried the bsdiff approach on a new .ccache folder obtained from compiling
8 linux kernels (various v3.x versions) configured as allnoconfig.

Stats:
- 22MB
- 4030 objects (gzipped)
- 503 cache hits

After applying diffs:
- 8.4MB (1862 files) were selected for diffs
- 1.5MB (17%) compressed patches resulted
- applying the 1862 diffs took 1.21 sec (so 0.65ms per patch)

I got the 0.65ms figure using a script that runs gunzip -c and bspatch for
each patched file in the cache. To find out and subtract the overhead of
creating the gunzip and bspatch processes, I ran the loop with just
"gunzip; bspatch --help > null" instead of the normal "gunzip; bspatch". So
the result should cover just the actual patch applying (ccache does the
gunzip anyway).

The test was done on a 3.4Ghz i7-2600.

My conclusions are:
- performance impact should be negligible (when compressing files offline
-- e.g. when make aborts with "no space left on device")
- savings in size vary (7MB out of 22MB in this case)
- the patches will need to have the "old filename" embedded

If people post results from their cache folders and there is interest, I
can work on an implementation.

Attached are the updated sources for the test. I normally run them in a
"testccache" directory, since they create two more folders with data.

(I posted this mail privately just to Andrew by mistake 3 days ago,
reposting it to ccache-list now)

Cheers,
Bogdan

_______________________________________________
ccache mailing list
[email protected]
https://lists.samba.org/mailman/listinfo/ccache
bspatch-all.sh (application/x-sh, 183 B)
#!/bin/sh

BSPATCH=bspatch
#BSPATCH=~/build/bsdiff-4.3/bspatch

cd patches

for f in `find .ccache -type f`; do
	gunzip -c ~/$f > ../a.o
	$BSPATCH ../a.o /dev/null $f
done

rm ../a.o
find-similar.cpp (text/x-c++src, 4.8 KB)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <ctype.h>

const char *BSDIFF = "bsdiff";
//const char *BSDIFF = "~/build/bsdiff-4.3/bsdiff";

struct Func {
	Func *next;

	char *name;
};

struct Obj {
	Obj *next;

	char *name;
	char *expanded;
	int nfuncs;
	Func *funcs;
	
	Obj *match;
	int score;
};

const int HASH_SIZE = 19031;
Obj *objs[HASH_SIZE] = {};
#define NAME_HASH(name) ((*(int *)(name)) % HASH_SIZE)

Obj *read_obj(const char *strbase, int a, int b, const char *namesuff)
{
	Obj *o = new Obj;
	o->next = 0;
	o->name = new char[64];
	o->expanded = new char[128];
	o->nfuncs = 0;
	o->funcs = 0;
	o->match = 0;
	o->score = 0;
	
	o->name = strdup(namesuff);

	int nc = snprintf(o->expanded, 128, "%x/%x/%s", a, b, namesuff);
	if (nc <= 0 || nc >= 64) abort();
	
	char strpath[PATH_MAX];
	sprintf(strpath, "%s/%s", strbase, namesuff);

	FILE *f = fopen(strpath, "rb");
	if (!f) {
		printf("open %s\n", namesuff);
		abort();
	}

	for (;;) {
		static char line[PATH_MAX];
		if (!fgets(line, PATH_MAX, f))
			break;

		for (int l=strlen(line); l && isspace(line[l-1]); line[--l]=0);


		Func **h = &o->funcs;
		
		Func *fn = new Func;
		fn->name = strdup(line);
		
		fn->next = *h;
		*h = fn;

		o->nfuncs++;
	}

	fclose(f);

	return o;
}

#define max(a,b) (((a) > (b)) ? (a) : (b))

void compare(Obj *o1, Obj *o2)
{
	/*
	if (strcmp(o1->name, "978911edd002afb6ecb9611659327e3e-3475537") ||
		strcmp(o2->name, "4794bbcacf3bc42cdf1a44cf89523949-3429991"))
		return;
	*/

	if (o1->nfuncs < 4 || o2->nfuncs < 4) return;
	int r = 100*o1->nfuncs/o2->nfuncs;
	if (r < 80 || r > 125) return;
	if (!strcmp(o1->name, o2->name)) return;

	Func *f1 = o2->funcs;
	Func *f2 = o1->funcs;
	
	int m=0;
	while (f1) {
		int r = 0;
		while (f2 && (r = strcmp(f1->name, f2->name)) < 0) // reversed since we pushed fns at the head
			f2 = f2->next;

		if (f2 && r==0) // match
			m++;

		f1 = f1->next;
	}
	
	int score = 1000*m/max(o1->nfuncs, o2->nfuncs);
	if (score > 800 && score > o1->score) {
		o1->score = score;
		o1->match = o2;
	}
}

int mk_file_dirs(char *file)
{
	char *ptr = file;

	for (;;) {
		ptr = strchr(ptr, '/');
		if (!ptr) return 0;

		*ptr = 0;
		int ret = mkdir(file, 0750);
		*ptr = '/';

		if (ret < 0 && errno != EEXIST) {
			fprintf(stderr, "%s: errno=%s (%d)\n", file, strerror(errno), errno);
			return -1;
		}

		ptr++;
	}
}

int main()
{
	int nobjs=0;

	for (int a=0; a<0x10; a++)
		for (int b=0; b<0x10; b++) {
			char strbase[PATH_MAX];
			sprintf(strbase, "strings/.ccache/%x/%x", a, b);

			DIR *dh = opendir(strbase);
			if (!dh) continue;

			for (;;) {
				struct dirent *de = readdir(dh);
				if (!de)
					break;
				if (strchr(de->d_name, '.'))
					continue;
				
				Obj *o = read_obj(strbase, a, b, de->d_name);
				
				Obj **h = &objs[NAME_HASH(o->name)];
				o->next = *h;
				*h = o;

				nobjs++;
			}

			closedir(dh);
		}
	
	long long total_raw=0;
	long long total_bsd=0;
	int nbsd=0;

	for (int h1=0; h1<HASH_SIZE; h1++) {
		fprintf(stderr, "\r%d%%", h1*100/HASH_SIZE);
		fflush(stderr);

		for (Obj *o1=objs[h1]; o1; o1=o1->next) {
			// Look for a match in all the objects that are after it in our hash.
			// If we get a match, it means we can store o1 as a delta from o2.

			for (int h2=h1; h2<HASH_SIZE; h2++) {
				Obj *o2 = (h2==h1) ? o1 : objs[h2];
				
				for (; o2; o2=o2->next)
					compare(o1, o2);
			}

			if (o1->match) {
				char bin1[PATH_MAX];
				char bin2[PATH_MAX];
				sprintf(bin1, "%s/.ccache/%s", getenv("HOME"), o1->expanded);
				sprintf(bin2, "%s/.ccache/%s", getenv("HOME"), o1->match->expanded);

				char cmd[PATH_MAX];

				sprintf(cmd, "gunzip -c %s > a.o", bin1);
				if (system(cmd)) { printf("%s\n", cmd); abort(); }

				sprintf(cmd, "gunzip -c %s > b.o", bin2);
				if (system(cmd)) { printf("%s\n", cmd); abort(); }
				
				char patch[PATH_MAX];
				sprintf(patch, "patches/.ccache/%s", o1->expanded);

				if (mk_file_dirs(patch))
					abort();

				snprintf(cmd, PATH_MAX, "%s a.o b.o %s", BSDIFF, patch);
				if (system(cmd)) { printf("%s\n", cmd); abort(); }
				
				struct stat st;

				if (stat(bin1, &st))
					{ printf("stat %s\n", bin1); abort(); }
				off_t s1 = st.st_size;

				if (stat(patch, &st))
					{ printf("%s\n", patch); abort(); }
				off_t sd = st.st_size;
			
				/*if (stat(bin2, &st))
					{ printf("stat %s\n", bin2); abort(); }
				off_t s2 = st.st_size;

				printf("r %ld \tsc %d\tn1 %d\tn2 %d\ts1 %ld\ts2 %ld\tsd %ld\t%s %s\n", 
						sd*100/max(s1, s2),
						o1->score, o1->nfuncs, o1->match->nfuncs, 
						s1, s2, sd,
						o1->expanded, o1->match->expanded);*/

				total_raw += s1;
				total_bsd += sd;
	
				nbsd++;
			}
		}
	}

	remove("a.o");
	remove("b.o");

	fprintf(stderr, "\n%d files compressed (%lld -> %lld bytes, %lld%%) \n", nbsd, total_raw, total_bsd, 100*total_bsd/total_raw);
	
	return 0;
}
get-symbols.sh (application/x-sh, 369 B)
#!/bin/bash

MYDIR=$PWD


cd ~

N=`find .ccache -name '*-*.stderr' | wc -l`
I=0

export LC_ALL=C # for sort

for f in `find .ccache -name '*-*.stderr' | cut -d . -f 1-2`; do
	let I+=1
	echo -en "\r$I/$N"
	mkdir -p $MYDIR/strings/`dirname $f`
	gunzip -c $f > tmp.o
	objdump -t tmp.o | grep " g " | awk '{print $NF}' | sort | uniq > $MYDIR/strings/$f
done

rm tmp.o
echo
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.