From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
Hi,
Here is a long overdue resend and improvement of the jh/notes topic in 'pu'.
The 5 first patches are pretty much unchanged (except for better attribution
of the various people who have helped improve this patch series).
The 6th patch introduces a first draft of notes tree parsing with support for
fanout subtrees. This first draft is just a straightforward implementation of
what I have picked up from the (many) discussions on this topic. As such,
this first draft focuses on correctness, rather than performance. BTW, did I
mention this was a first draft?
The 7th patch is stolen from the jh/vcs-cvs topic in 'pu', and teaches
git-fast-import to import note objects.
The final 8th patch is a relatively straightforward optimization of t3302.
Have fun! :)
...Johan
Johan Herland (4):
Teach "-m <msg>" and "-F <file>" to "git notes edit"
First draft of notes tree parser with support for fanout subtrees
fast-import: Add support for importing commit notes
t3302-notes-index-expensive: Speed up create_repo()
Johannes Schindelin (4):
Introduce commit notes
Add a script to edit/inspect notes
Speed up git notes lookup
Add an expensive test for git-notes
.gitignore | 1 +
Documentation/config.txt | 13 ++
Documentation/git-fast-import.txt | 45 +++++-
Documentation/git-notes.txt | 60 ++++++++
Makefile | 3 +
cache.h | 4 +
command-list.txt | 1 +
commit.c | 1 +
config.c | 5 +
environment.c | 1 +
fast-import.c | 88 +++++++++++-
git-notes.sh | 121 +++++++++++++++
notes.c | 295 +++++++++++++++++++++++++++++++++++++
notes.h | 7 +
pretty.c | 5 +
t/t3301-notes.sh | 149 +++++++++++++++++++
t/t3302-notes-index-expensive.sh | 118 +++++++++++++++
t/t3303-notes-subtrees.sh | 206 ++++++++++++++++++++++++++
t/t9300-fast-import.sh | 166 +++++++++++++++++++++
19 files changed, 1279 insertions(+), 10 deletions(-)
create mode 100644 Documentation/git-notes.txt
create mode 100755 git-notes.sh
create mode 100644 notes.c
create mode 100644 notes.h
create mode 100755 t/t3301-notes.sh
create mode 100755 t/t3302-notes-index-expensive.sh
create mode 100755 t/t3303-notes-subtrees.sh
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
From: Johannes Schindelin <redacted>
Commit notes are blobs which are shown together with the commit
message. These blobs are taken from the notes ref, which you can
configure by the config variable core.notesRef, which in turn can
be overridden by the environment variable GIT_NOTES_REF.
The notes ref is a branch which contains "files" whose names are
the names of the corresponding commits (i.e. the SHA-1).
The rationale for putting this information into a ref is this: we
want to be able to fetch and possibly union-merge the notes,
maybe even look at the date when a note was introduced, and we
want to store them efficiently together with the other objects.
This patch has been improved by the following contributions:
- Thomas Rast: fix core.notesRef documentation
- Tor Arne Vestbø: fix printing of multi-line notes
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Thomas Rast <redacted>
Signed-off-by: Tor Arne Vestbø <redacted>
Signed-off-by: Johan Herland <redacted>
Signed-off-by: Junio C Hamano <redacted>
---
Documentation/config.txt | 13 ++++++++
Makefile | 2 +
cache.h | 4 ++
commit.c | 1 +
config.c | 5 +++
environment.c | 1 +
notes.c | 69 ++++++++++++++++++++++++++++++++++++++++++++++
notes.h | 7 ++++
pretty.c | 5 +++
9 files changed, 107 insertions(+), 0 deletions(-)
create mode 100644 notes.c
create mode 100644 notes.h
@@ -439,6 +439,19 @@ On some file system/operating system combinations, this is unreliable. Set this config setting to 'rename' there; However, This will remove the check that makes sure that existing object files will not get overwritten.+core.notesRef::+ When showing commit messages, also show notes which are stored in+ the given ref. This ref is expected to contain files named+ after the full SHA-1 of the commit they annotate.+++If such a file exists in the given ref, the referenced blob is read, and+appended to the commit message, separated by a "Notes:" line. If the+given ref itself does not exist, it is not an error, but means that no+notes should be printed.+++This setting defaults to "refs/notes/commits", and can be overridden by+the `GIT_NOTES_REF` environment variable.+ add.ignore-errors:: Tells 'git-add' to continue adding files when some files cannot be added due to indexing errors. Equivalent to the '--ignore-errors'
@@ -0,0 +1,69 @@+#include"cache.h"+#include"commit.h"+#include"notes.h"+#include"refs.h"+#include"utf8.h"+#include"strbuf.h"++staticintinitialized;++voidget_commit_notes(conststructcommit*commit,structstrbuf*sb,+constchar*output_encoding)+{+staticconstchar*utf8="utf-8";+structstrbufname=STRBUF_INIT;+constchar*hex;+unsignedcharsha1[20];+char*msg,*msg_p;+unsignedlonglinelen,msglen;+enumobject_typetype;++if(!initialized){+constchar*env=getenv(GIT_NOTES_REF_ENVIRONMENT);+if(env)+notes_ref_name=getenv(GIT_NOTES_REF_ENVIRONMENT);+elseif(!notes_ref_name)+notes_ref_name=GIT_NOTES_DEFAULT_REF;+if(notes_ref_name&&read_ref(notes_ref_name,sha1))+notes_ref_name=NULL;+initialized=1;+}++if(!notes_ref_name)+return;++strbuf_addf(&name,"%s:%s",notes_ref_name,+sha1_to_hex(commit->object.sha1));+if(get_sha1(name.buf,sha1))+return;++if(!(msg=read_sha1_file(sha1,&type,&msglen))||!msglen||+type!=OBJ_BLOB)+return;++if(output_encoding&&*output_encoding&&+strcmp(utf8,output_encoding)){+char*reencoded=reencode_string(msg,output_encoding,utf8);+if(reencoded){+free(msg);+msg=reencoded;+msglen=strlen(msg);+}+}++/* we will end the annotation by a newline anyway */+if(msglen&&msg[msglen-1]=='\n')+msglen--;++strbuf_addstr(sb,"\nNotes:\n");++for(msg_p=msg;msg_p<msg+msglen;msg_p+=linelen+1){+linelen=strchrnul(msg_p,'\n')-msg_p;++strbuf_addstr(sb," ");+strbuf_add(sb,msg_p,linelen);+strbuf_addch(sb,'\n');+}++free(msg);+}
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
From: Johannes Schindelin <redacted>
The script 'git notes' allows you to edit and show commit notes, by
calling either
git notes show <commit>
or
git notes edit <commit>
This patch has been improved by the following contributions:
- Tor Arne Vestbø: fix printing of multi-line notes
- Michael J Gruber: test and handle empty notes gracefully
- Thomas Rast:
- only clean up message file when editing
- use GIT_EDITOR and core.editor over VISUAL/EDITOR
- t3301: fix confusing quoting in test for valid notes ref
- t3301: use test_must_fail instead of !
- refuse to edit notes outside refs/notes/
- Junio C Hamano: tests: fix "export var=val"
- Christian Couder: documentation: fix 'linkgit' macro in "git-notes.txt"
- Johan Herland: minor cleanup and bugfixing in git-notes.sh (v2)
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Tor Arne Vestbø <redacted>
Signed-off-by: Michael J Gruber <redacted>
Signed-off-by: Thomas Rast <redacted>
Signed-off-by: Christian Couder <redacted>
Signed-off-by: Johan Herland <redacted>
Signed-off-by: Junio C Hamano <redacted>
---
.gitignore | 1 +
Documentation/git-notes.txt | 46 +++++++++++++++++
Makefile | 1 +
command-list.txt | 1 +
git-notes.sh | 73 +++++++++++++++++++++++++++
t/t3301-notes.sh | 114 +++++++++++++++++++++++++++++++++++++++++++
6 files changed, 236 insertions(+), 0 deletions(-)
create mode 100644 Documentation/git-notes.txt
create mode 100755 git-notes.sh
create mode 100755 t/t3301-notes.sh
@@ -0,0 +1,46 @@+git-notes(1)+============++NAME+----+git-notes - Add/inspect commit notes++SYNOPSIS+--------+[verse]+'git-notes' (edit | show) [commit]++DESCRIPTION+-----------+This command allows you to add notes to commit messages, without+changing the commit. To discern these notes from the message stored+in the commit object, the notes are indented like the message, after+an unindented line saying "Notes:".++To disable commit notes, you have to set the config variable+core.notesRef to the empty string. Alternatively, you can set it+to a different ref, something like "refs/notes/bugzilla". This setting+can be overridden by the environment variable "GIT_NOTES_REF".+++SUBCOMMANDS+-----------++edit::+ Edit the notes for a given commit (defaults to HEAD).++show::+ Show the notes for a given commit (defaults to HEAD).+++Author+------+Written by Johannes Schindelin <johannes.schindelin@gmx.de>++Documentation+-------------+Documentation by Johannes Schindelin++GIT+---+Part of the linkgit:git[7] suite
@@ -0,0 +1,73 @@+#!/bin/sh++USAGE="(edit | show) [commit]"+.git-sh-setup++test-n"$3"&&usage++test-z"$1"&&usage+ACTION="$1";shift++test-z"$GIT_NOTES_REF"&&GIT_NOTES_REF="$(gitconfigcore.notesref)"+test-z"$GIT_NOTES_REF"&&GIT_NOTES_REF="refs/notes/commits"++COMMIT=$(gitrev-parse--verify--defaultHEAD"$@")||+die"Invalid commit: $@"++case"$ACTION"in+edit)+if["${GIT_NOTES_REF#refs/notes/}"="$GIT_NOTES_REF"];then+die"Refusing to edit notes in $GIT_NOTES_REF (outside of refs/notes/)"+fi++MSG_FILE="$GIT_DIR/new-notes-$COMMIT"+GIT_INDEX_FILE="$MSG_FILE.idx"+exportGIT_INDEX_FILE++trap'+test-f"$MSG_FILE"&&rm"$MSG_FILE"+test-f"$GIT_INDEX_FILE"&&rm"$GIT_INDEX_FILE"+'0++GIT_NOTES_REF=gitlog-1$COMMIT|sed"s/^/#/">"$MSG_FILE"++CURRENT_HEAD=$(gitshow-ref"$GIT_NOTES_REF"|cut-f1-d' ')+if[-z"$CURRENT_HEAD"];then+PARENT=+else+PARENT="-p $CURRENT_HEAD"+gitread-tree"$GIT_NOTES_REF"||die"Could not read index"+gitcat-fileblob:$COMMIT>>"$MSG_FILE"2>/dev/null+fi++core_editor="$(gitconfigcore.editor)"+${GIT_EDITOR:-${core_editor:-${VISUAL:-${EDITOR:-vi}}}}"$MSG_FILE"++grep-v^#<"$MSG_FILE"|gitstripspace>"$MSG_FILE".processed+mv"$MSG_FILE".processed"$MSG_FILE"+if[-s"$MSG_FILE"];then+BLOB=$(githash-object-w"$MSG_FILE")||+die"Could not write into object database"+gitupdate-index--add--cacheinfo0644$BLOB$COMMIT||+die"Could not write index"+else+test-z"$CURRENT_HEAD"&&+die"Will not initialise with empty tree"+gitupdate-index--force-remove$COMMIT||+die"Could not update index"+fi++TREE=$(gitwrite-tree)||die"Could not write tree"+NEW_HEAD=$(echoAnnotate$COMMIT|gitcommit-tree$TREE$PARENT)||+die"Could not annotate"+gitupdate-ref-m"Annotate $COMMIT"\+"$GIT_NOTES_REF"$NEW_HEAD$CURRENT_HEAD+;;+show)+gitrev-parse-q--verify"$GIT_NOTES_REF":$COMMIT>/dev/null||+die"No note for commit $COMMIT."+gitshow"$GIT_NOTES_REF":$COMMIT+;;+*)+usage+esac
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
From: Johannes Schindelin <redacted>
To avoid looking up each and every commit in the notes ref's tree
object, which is very expensive, speed things up by slurping the tree
object's contents into a hash_map.
The idea for the hashmap singleton is from David Reiss, initial
benchmarking by Jeff King.
Note: the implementation allows for arbitrary entries in the notes
tree object, ignoring those that do not reference a valid object. This
allows you to annotate arbitrary branches, or objects.
This patch has been improved by the following contributions:
- Junio C Hamano: fixed an obvious error in initialize_hash_map()
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Johan Herland <redacted>
Signed-off-by: Junio C Hamano <redacted>
---
notes.c | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 files changed, 102 insertions(+), 11 deletions(-)
@@ -4,16 +4,112 @@#include"refs.h"#include"utf8.h"#include"strbuf.h"+#include"tree-walk.h"++structentry{+unsignedcharcommit_sha1[20];+unsignedcharnotes_sha1[20];+};++structhash_map{+structentry*entries;+off_tcount,size;+};staticintinitialized;+staticstructhash_maphash_map;++staticinthash_index(structhash_map*map,constunsignedchar*sha1)+{+inti=((*(unsignedint*)sha1)%map->size);++for(;;){+unsignedchar*current=map->entries[i].commit_sha1;++if(!hashcmp(sha1,current))+returni;++if(is_null_sha1(current))+return-1-i;++if(++i==map->size)+i=0;+}+}++staticvoidadd_entry(constunsignedchar*commit_sha1,+constunsignedchar*notes_sha1)+{+intindex;++if(hash_map.count+1>hash_map.size>>1){+inti,old_size=hash_map.size;+structentry*old=hash_map.entries;++hash_map.size=old_size?old_size<<1:64;+hash_map.entries=(structentry*)+xcalloc(sizeof(structentry),hash_map.size);++for(i=0;i<old_size;i++)+if(!is_null_sha1(old[i].commit_sha1)){+index=-1-hash_index(&hash_map,+old[i].commit_sha1);+memcpy(hash_map.entries+index,old+i,+sizeof(structentry));+}+free(old);+}++index=hash_index(&hash_map,commit_sha1);+if(index<0){+index=-1-index;+hash_map.count++;+}++hashcpy(hash_map.entries[index].commit_sha1,commit_sha1);+hashcpy(hash_map.entries[index].notes_sha1,notes_sha1);+}++staticvoidinitialize_hash_map(constchar*notes_ref_name)+{+unsignedcharsha1[20],commit_sha1[20];+unsignedmode;+structtree_descdesc;+structname_entryentry;+void*buf;++if(!notes_ref_name||read_ref(notes_ref_name,commit_sha1)||+get_tree_entry(commit_sha1,"",sha1,&mode))+return;++buf=fill_tree_descriptor(&desc,sha1);+if(!buf)+die("Could not read %s for notes-index",sha1_to_hex(sha1));++while(tree_entry(&desc,&entry))+if(!get_sha1(entry.path,commit_sha1))+add_entry(commit_sha1,entry.sha1);+free(buf);+}++staticunsignedchar*lookup_notes(constunsignedchar*commit_sha1)+{+intindex;++if(!hash_map.size)+returnNULL;++index=hash_index(&hash_map,commit_sha1);+if(index<0)+returnNULL;+returnhash_map.entries[index].notes_sha1;+}voidget_commit_notes(conststructcommit*commit,structstrbuf*sb,constchar*output_encoding){staticconstchar*utf8="utf-8";-structstrbufname=STRBUF_INIT;-constchar*hex;-unsignedcharsha1[20];+unsignedchar*sha1;char*msg,*msg_p;unsignedlonglinelen,msglen;enumobject_typetype;
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
From: Johannes Schindelin <redacted>
git-notes have the potential of being pretty expensive, so test with
a lot of commits. A lot. So to make things cheaper, you have to
opt-in explicitely, by setting the environment variable
GIT_NOTES_TIMING_TESTS.
This patch has been improved by the following contributions:
- Junio C Hamano: tests: fix "export var=val"
Signed-off-by: Johannes Schindelin <redacted>
Signed-off-by: Johan Herland <redacted>
Signed-off-by: Junio C Hamano <redacted>
---
t/t3302-notes-index-expensive.sh | 98 ++++++++++++++++++++++++++++++++++++++
1 files changed, 98 insertions(+), 0 deletions(-)
create mode 100755 t/t3302-notes-index-expensive.sh
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
The "-m" and "-F" options are already the established method
(in both git-commit and git-tag) to specify a commit/tag message
without invoking the editor. This patch teaches "git notes edit"
to respect the same options for specifying a notes message without
invoking the editor.
Multiple "-m" and/or "-F" options are concatenated as separate
paragraphs.
The patch also updates the "git notes" documentation and adds
selftests for the new functionality. Unfortunately, the added
selftests include a couple of lines with trailing whitespace
(without these the test will fail). This may cause git to warn
about "whitespace errors".
Signed-off-by: Johan Herland <redacted>
---
Documentation/git-notes.txt | 16 ++++++++++-
git-notes.sh | 64 +++++++++++++++++++++++++++++++++++++-----
t/t3301-notes.sh | 35 +++++++++++++++++++++++
3 files changed, 106 insertions(+), 9 deletions(-)
@@ -33,6 +33,20 @@ show:: Show the notes for a given commit (defaults to HEAD).+OPTIONS+-------+-m <msg>::+ Use the given note message (instead of prompting).+ If multiple `-m` (or `-F`) options are given, their+ values are concatenated as separate paragraphs.++-F <file>::+ Take the note message from the given file. Use '-' to+ read the note message from the standard input.+ If multiple `-F` (or `-m`) options are given, their+ values are concatenated as separate paragraphs.++ Author ------ Written by Johannes Schindelin <johannes.schindelin@gmx.de>
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
This is a relatively straightforward implementation of parsing notes trees
that use fanout directories to limit the size of individual tree objects.
This first draft uses a simple linked list for holding unparsed subtree
references (to be parsed on demand), and as such, this first draft
concentrates more on correctness than performance (AFAICS from t3302, there
is no measurable performance impact when no fanout subtrees are present).
The semantics used when parsing notes trees (with regards to fanout subtrees)
follow Dscho's proposal fairly closely:
- No concatenation/merging of notes is performed. If there are several notes
objects referencing a given commit, only one of those objects are used.
- If a notes object for a given commit is present in the "root" notes tree,
no subtrees are consulted; the object in the root tree is used directly.
- If there are more than one subtree that prefix-matches the given commit,
only the subtree with the _longest_ matching prefix is consulted. This
means that if the given commit is e.g. "deadbeef", and the notes tree have
subtrees "de" and "dead", then the following paths in the notes tree are
searched: "deadbeef", "dead/beef". Note that "de/adbeef" is NOT searched.
(This might change in the future)
- Fanout directories (subtrees) must references a whole number of bytes
from the SHA1 sum they subdivide. E.g. subtrees "dead" and "de" are
acceptable; "d" and "dea" are not.
- Multiple levels of fanout is allowed. All the above rules apply recursively.
E.g. "de/adbeef" is preferred over "de/adbe/ef", etc.
The patch includes new selftests for verifying the expected behaviour when
loading notes trees with various fanout schemes.
Cc: Johannes Schindelin <redacted>
Signed-off-by: Johan Herland <redacted>
---
notes.c | 165 ++++++++++++++++++++++++++++++++----
t/t3303-notes-subtrees.sh | 206 +++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 356 insertions(+), 15 deletions(-)
create mode 100755 t/t3303-notes-subtrees.sh
@@ -70,39 +81,163 @@ static void add_entry(const unsigned char *commit_sha1,hashcpy(hash_map.entries[index].notes_sha1,notes_sha1);}+/*+*ConvertapartialSHA1sum(hexformat)toaSHA1value.+*-hex-ASCIIhexSHA1segment+*-hex_len-Lengthofabovesegment.Mustbemultipleof2between0and40+*-sha1-ValueofSHA1iswrittenhere+*-sha1_len-Max#bytestostoreinsha1,Mustbebetween0and20,+*and>=hex_len/2+*Returns-1onerror(invalidargumentsorinvalidASCIIhexSHA1format).+*Otherwise,returnsnumberofbyteswrittentosha1(hex_len/2).+*Padssha1withNULsuptosha1_len(notincludedinreturnedlength).+*/+staticintget_sha1_hex_segment(constchar*hex,unsignedinthex_len,+unsignedchar*sha1,unsignedintsha1_len)+{+unsignedinti,len=hex_len>>1;+if(hex_len%2!=0||len>sha1_len)+return-1;+for(i=0;i<len;i++){+unsignedintval=(hexval(hex[0])<<4)|hexval(hex[1]);+if(val&~0xff)+return-1;+*sha1++=val;+hex+=2;+}+for(;i<sha1_len;i++)+*sha1++=0;+returnlen;+}++staticvoidload_subtree(structsubtree_entry*se)+{+unsignedcharcommit_sha1[20];+unsignedintprefix_len;+void*buf;+structtree_descdesc;+structname_entryentry;+structsubtree_entry*tmp_list=NULL,*tmp_last=NULL;++buf=fill_tree_descriptor(&desc,se->subtree_sha1);+if(!buf)+die("Could not read %s for notes-index",+sha1_to_hex(se->subtree_sha1));++prefix_len=se->sha1_prefix_w_len[19];+memcpy(commit_sha1,se->sha1_prefix_w_len,prefix_len);+while(tree_entry(&desc,&entry)){+intlen=get_sha1_hex_segment(entry.path,strlen(entry.path),+commit_sha1+prefix_len,20-prefix_len);+if(len<0)+continue;/* entry.path is not a SHA1 sum. Skip */+len+=prefix_len;++/* If commit SHA1 is complete, assume note object */+if(len==20)+add_entry(commit_sha1,entry.sha1);+/* If commit SHA1 is incomplete, assume note subtree */+elseif(len<20&&entry.mode==S_IFDIR){+structsubtree_entry*n=(structsubtree_entry*)+xcalloc(sizeof(structsubtree_entry),1);+hashcpy(n->sha1_prefix_w_len,commit_sha1);+n->sha1_prefix_w_len[19]=(unsignedchar)len;+hashcpy(n->subtree_sha1,entry.sha1);++if(!tmp_list){+tmp_list=n;+tmp_last=n;+}+else{+assert(!tmp_last->next);+assert(hashcmp(n->sha1_prefix_w_len,+tmp_last->sha1_prefix_w_len)>0);+tmp_last->next=n;+tmp_last=n;+}+}+}+free(buf);+if(tmp_list){+/* insert tmp_list immediately after se */+assert(hashcmp(tmp_list->sha1_prefix_w_len,+se->sha1_prefix_w_len)>0);+if(se->next){+assert(hashcmp(se->next->sha1_prefix_w_len,+tmp_last->sha1_prefix_w_len)>0);+tmp_last->next=se->next;+}+se->next=tmp_list;+}+}+staticvoidinitialize_hash_map(constchar*notes_ref_name){unsignedcharsha1[20],commit_sha1[20];unsignedmode;-structtree_descdesc;-structname_entryentry;-void*buf;+structsubtree_entryroot_tree;if(!notes_ref_name||read_ref(notes_ref_name,commit_sha1)||get_tree_entry(commit_sha1,"",sha1,&mode))return;-buf=fill_tree_descriptor(&desc,sha1);-if(!buf)-die("Could not read %s for notes-index",sha1_to_hex(sha1));+hashclr(root_tree.sha1_prefix_w_len);+hashcpy(root_tree.subtree_sha1,sha1);+root_tree.next=NULL;+load_subtree(&root_tree);+subtree_list=root_tree.next;+}-while(tree_entry(&desc,&entry))-if(!get_sha1(entry.path,commit_sha1))-add_entry(commit_sha1,entry.sha1);-free(buf);+/*+*ComparethegivencommitSHA1againstthegivensubtreeentry.+*Return-1ifthecommitSHA1cannotexistwithinthegivensubtree,orany+*subtreefollowingit.+*Return0ifthecommitSHA1_may_existwithinthegivensubtree.+*Return1ifthecommitSHA1cannotexistwithinthegivensubtree,butmay+*existwithinasubtreefollowingit.+*/+staticintcommit_subtree_cmp(constunsignedchar*commit_sha1,+conststructsubtree_entry*entry)+{+unsignedintprefix_len=entry->sha1_prefix_w_len[19];+returnmemcmp(commit_sha1,entry->sha1_prefix_w_len,prefix_len);+}++staticstructsubtree_entry*lookup_subtree(constunsignedchar*commit_sha1)+{+structsubtree_entry*found=NULL,*cur=subtree_list;+while(cur){+intcmp=commit_subtree_cmp(commit_sha1,cur);+if(!cmp)+found=cur;+if(cmp<0)+break;+cur=cur->next;+}+returnfound;}staticunsignedchar*lookup_notes(constunsignedchar*commit_sha1){intindex;+structsubtree_entry*subtree;-if(!hash_map.size)-returnNULL;+/* First, try to find the commit SHA1 directly in hash map */+index=hash_map.size?hash_index(&hash_map,commit_sha1):-1;+if(index>=0)+returnhash_map.entries[index].notes_sha1;-index=hash_index(&hash_map,commit_sha1);-if(index<0)+/* Next, try finding a subtree that may contain the commit SHA1 */+subtree=lookup_subtree(commit_sha1);++/* Give up if no subtree found, or if subtree is already loaded */+if(!subtree||is_null_sha1(subtree->subtree_sha1))returnNULL;-returnhash_map.entries[index].notes_sha1;++/* Load subtree into hash_map, and retry lookup recursively */+load_subtree(subtree);+hashclr(subtree->subtree_sha1);+returnlookup_notes(commit_sha1);}voidget_commit_notes(conststructcommit*commit,structstrbuf*sb,
@@ -0,0 +1,206 @@+#!/bin/sh++test_description='Test commit notes organized in subtrees'++../test-lib.sh++number_of_commits=100++start_note_commit(){+test_tick&&+cat<<INPUT_END+commitrefs/notes/commits+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+data<<COMMIT+notes+COMMIT++fromrefs/notes/commits^0+deleteall+INPUT_END++}++verify_notes(){+gitlog|grep"^ ">output&&+i=$number_of_commits&&+while[$i-gt0];do+echo" commit #$i"&&+echo" note for commit #$i"&&+i=$(($i-1));+done>expect&&+test_cmpexpectoutput+}++test_expect_success'setup: create $number_of_commits commits''++(+nr=0&&+while[$nr-lt$number_of_commits];do+nr=$(($nr+1))&&+test_tick&&+cat<<INPUT_END+commitrefs/heads/master+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+data<<COMMIT+commit#$nr+COMMIT++M644inlinefile+data<<EOF+fileincommit#$nr+EOF++INPUT_END++done&&+test_tick&&+cat<<INPUT_END+commitrefs/notes/commits+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+data<<COMMIT+nonotes+COMMIT++deleteall++INPUT_END++)|+gitfast-import--quiet&&+gitconfigcore.notesRefrefs/notes/commits+'++test_expect_success'test notes in 2/38-fanout''++(+start_note_commit&&+nr=$number_of_commits&&+gitrev-listrefs/heads/master|+whilereadsha1;do+note_path=$(echo"$sha1"|sed"s|^..|&/|")+cat<<INPUT_END&&+M100644inline$note_path+data<<EOF+noteforcommit#$nr+EOF++INPUT_END++nr=$(($nr-1))+done+)|+gitfast-import--quiet+'++test_expect_success'verify notes in 2/38-fanout''verify_notes'++test_expect_success'test notes in 4/36-fanout''++(+start_note_commit&&+nr=$number_of_commits&&+gitrev-listrefs/heads/master|+whilereadsha1;do+note_path=$(echo"$sha1"|sed"s|^....|&/|")+cat<<INPUT_END&&+M100644inline$note_path+data<<EOF+noteforcommit#$nr+EOF++INPUT_END++nr=$(($nr-1))+done+)|+gitfast-import--quiet+'++test_expect_success'verify notes in 4/36-fanout''verify_notes'++test_expect_success'test notes in 4/36-fanout overriding 2/38-fanout''++(+start_note_commit&&+nr=$number_of_commits&&+gitrev-listrefs/heads/master|+whilereadsha1;do+ignored_note_path=$(echo"$sha1"|sed"s|^..|&/|")+preferred_note_path=$(echo"$sha1"|sed"s|^....|&/|")+cat<<INPUT_END&&+M100644inline$ignored_note_path+data<<EOF+IGNOREDnoteforcommit#$nr+EOF++M100644inline$preferred_note_path+data<<EOF+noteforcommit#$nr+EOF++INPUT_END++nr=$(($nr-1))+done+)|+gitfast-import--quiet+'++test_expect_success'verify notes in 4/36-fanout overriding 2/38-fanout''verify_notes'++test_expect_success'test notes in 2/2/36-fanout''++(+start_note_commit&&+nr=$number_of_commits&&+gitrev-listrefs/heads/master|+whilereadsha1;do+note_path=$(echo"$sha1"|sed"s|^\(..\)\(..\)|\1/\2/|")+cat<<INPUT_END&&+M100644inline$note_path+data<<EOF+noteforcommit#$nr+EOF++INPUT_END++nr=$(($nr-1))+done+)|+gitfast-import--quiet+'++test_expect_success'verify notes in 2/2/36-fanout''verify_notes'++test_expect_success'test notes in 2/38-fanout overriding 2/2/36-fanout''++(+start_note_commit&&+nr=$number_of_commits&&+gitrev-listrefs/heads/master|+whilereadsha1;do+ignored_note_path=$(echo"$sha1"|sed"s|^\(..\)\(..\)|\1/\2/|")+preferred_note_path=$(echo"$sha1"|sed"s|^..|&/|")+cat<<INPUT_END&&+M100644inline$ignored_note_path+data<<EOF+IGNOREDnoteforcommit#$nr+EOF++M100644inline$preferred_note_path+data<<EOF+noteforcommit#$nr+EOF++INPUT_END++nr=$(($nr-1))+done+)|+gitfast-import--quiet+'++test_expect_success'verify notes in 2/38-fanout overriding 2/2/36-fanout''verify_notes'++test_done
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
Creating repos with 10/100/1000/10000 commits and notes takes a lot of time.
However, using git-fast-import to do the job is a lot more efficient than
using plumbing commands to do the same.
This patch decreases the overall run-time of this test on my machine from
~3 to ~1 minutes.
Signed-off-by: Johan Herland <redacted>
---
t/t3302-notes-index-expensive.sh | 74 ++++++++++++++++++++++++--------------
1 files changed, 47 insertions(+), 27 deletions(-)
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
Introduce a 'notemodify' subcommand of the 'commit' command. This subcommand
is similar to 'filemodify', except that no mode is supplied (all notes have
mode 0644), and the path is set to the hex SHA1 of the given "comittish".
This enables fast import of note objects along with their associated commits,
since the notes can now be named using the mark references of their
corresponding commits.
The patch also includes a test case of the added functionality.
Cc: Shawn O. Pearce <redacted>
Signed-off-by: Johan Herland <redacted>
---
Documentation/git-fast-import.txt | 45 +++++++++--
fast-import.c | 88 +++++++++++++++++++-
t/t9300-fast-import.sh | 166 +++++++++++++++++++++++++++++++++++++
3 files changed, 289 insertions(+), 10 deletions(-)
@@ -339,14 +339,13 @@ commit message use a 0 length data. Commit messages are free-form and are not interpreted by Git. Currently they must be encoded in UTF-8, as fast-import does not permit other encodings to be specified.-Zero or more `filemodify`, `filedelete`, `filecopy`, `filerename`-and `filedeleteall` commands+Zero or more `filemodify`, `filedelete`, `filecopy`, `filerename`,+`filedeleteall` and `notemodify` commands may be included to update the contents of the branch prior to creating the commit. These commands may be supplied in any order. However it is recommended that a `filedeleteall` command precede-all `filemodify`, `filecopy` and `filerename` commands in the same-commit, as `filedeleteall`-wipes the branch clean (see below).+all `filemodify`, `filecopy`, `filerename` and `notemodify` commands in+the same commit, as `filedeleteall` wipes the branch clean (see below). The `LF` after the command is optional (it used to be required).
@@ -595,6 +594,40 @@ more memory per active branch (less than 1 MiB for even most large projects); so frontends that can easily obtain only the affected paths for a commit are encouraged to do so.+`notemodify`+^^^^^^^^^^^^+Included in a `commit` command to add a new note (annotating a given+commit) or change the content of an existing note. This command has+two different means of specifying the content of the note.++External data format::+ The data content for the note was already supplied by a prior+ `blob` command. The frontend just needs to connect it to the+ commit that is to be annotated.+++....+ 'N' SP <dataref> SP <committish> LF+....+++Here `<dataref>` can be either a mark reference (`:<idnum>`)+set by a prior `blob` command, or a full 40-byte SHA-1 of an+existing Git blob object.++Inline data format::+ The data content for the note has not been supplied yet.+ The frontend wants to supply it as part of this modify+ command.+++....+ 'N' SP 'inline' SP <committish> LF+ data+....+++See below for a detailed description of the `data` command.++In both formats `<committish>` is any of the commit specification+expressions also accepted by `from` (see above).+ `mark` ~~~~~~ Arranges for fast-import to save a reference to the current object, allowing
@@ -22,8 +22,8 @@ Format of STDIN stream:('author'spnamesp'<'email'>'spwhenlf)?'committer'spnamesp'<'email'>'spwhenlfcommit_msg-('from'sp(ref_str|hexsha1|sha1exp_str|idnum)lf)?-('merge'sp(ref_str|hexsha1|sha1exp_str|idnum)lf)*+('from'spcommittishlf)?+('merge'spcommittishlf)*file_change*lf?;commit_msg::=data;
@@ -41,15 +41,18 @@ Format of STDIN stream:file_obm::='M'spmodesp(hexsha1|idnum)sppath_strlf;file_inm::='M'spmodesp'inline'sppath_strlfdata;+note_obm::='N'sp(hexsha1|idnum)spcommittishlf;+note_inm::='N'sp'inline'spcommittishlf+data;new_tag::='tag'sptag_strlf-'from'sp(ref_str|hexsha1|sha1exp_str|idnum)lf+'from'spcommittishlf('tagger'spnamesp'<'email'>'spwhenlf)?tag_msg;tag_msg::=data;reset_branch::='reset'spref_strlf-('from'sp(ref_str|hexsha1|sha1exp_str|idnum)lf)?+('from'spcommittishlf)?lf?;checkpoint::='checkpoint'lf
@@ -88,6 +91,7 @@ Format of STDIN stream:# stream formatting is: \, " and LF. Otherwise these values# are UTF8.#+committish::=(ref_str|hexsha1|sha1exp_str|idnum);ref_str::=ref;sha1exp_str::=sha1exp;tag_str::=tag;
@@ -2003,6 +2007,80 @@ static void file_change_cr(struct branch *b, int rename)leaf.tree);}+staticvoidnote_change_n(structbranch*b)+{+constchar*p=command_buf.buf+2;+staticstructstrbufuq=STRBUF_INIT;+structobject_entry*oe=oe;+structbranch*s;+unsignedcharsha1[20],commit_sha1[20];+uint16_tinline_data=0;++/* <dataref> or 'inline' */+if(*p==':'){+char*x;+oe=find_mark(strtoumax(p+1,&x,10));+hashcpy(sha1,oe->sha1);+p=x;+}elseif(!prefixcmp(p,"inline")){+inline_data=1;+p+=6;+}else{+if(get_sha1_hex(p,sha1))+die("Invalid SHA1: %s",command_buf.buf);+oe=find_object(sha1);+p+=40;+}+if(*p++!=' ')+die("Missing space after SHA1: %s",command_buf.buf);++/* <committish> */+s=lookup_branch(p);+if(s){+hashcpy(commit_sha1,s->sha1);+}elseif(*p==':'){+uintmax_tcommit_mark=strtoumax(p+1,NULL,10);+structobject_entry*commit_oe=find_mark(commit_mark);+if(commit_oe->type!=OBJ_COMMIT)+die("Mark :%"PRIuMAX" not a commit",commit_mark);+hashcpy(commit_sha1,commit_oe->sha1);+}elseif(!get_sha1(p,commit_sha1)){+unsignedlongsize;+char*buf=read_object_with_reference(commit_sha1,+commit_type,&size,commit_sha1);+if(!buf||size<46)+die("Not a valid commit: %s",p);+free(buf);+}else+die("Invalid ref name or SHA1 expression: %s",p);++if(inline_data){+staticstructstrbufbuf=STRBUF_INIT;++if(p!=uq.buf){+strbuf_addstr(&uq,p);+p=uq.buf;+}+read_next_command();+parse_data(&buf);+store_object(OBJ_BLOB,&buf,&last_blob,sha1,0);+}elseif(oe){+if(oe->type!=OBJ_BLOB)+die("Not a blob (actually a %s): %s",+typename(oe->type),command_buf.buf);+}else{+enumobject_typetype=sha1_object_info(sha1,NULL);+if(type<0)+die("Blob not found: %s",command_buf.buf);+if(type!=OBJ_BLOB)+die("Not a blob (actually a %s): %s",+typename(type),command_buf.buf);+}++tree_content_set(&b->branch_tree,sha1_to_hex(commit_sha1),sha1,+S_IFREG|0644,NULL);+}+staticvoidfile_change_deleteall(structbranch*b){release_tree_content_recursive(b->branch_tree.tree);
@@ -1088,4 +1088,170 @@ INPUT_END test_expect_success'P: fail on blob mark in gitlink''test_must_failgitfast-import<input'+###+### series Q (notes)+###++note1_data="Note for the first commit"+note2_data="Note for the second commit"+note3_data="Note for the third commit"++test_tick+cat>input<<INPUT_END+blob+mark:2+data<<EOF+$file2_data+EOF++commitrefs/heads/notes-test+mark:3+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+data<<COMMIT+first(:3)+COMMIT++M644:2file2++blob+mark:4+data$file4_len+$file4_data+commitrefs/heads/notes-test+mark:5+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+data<<COMMIT+second(:5)+COMMIT++M644:4file4++commitrefs/heads/notes-test+mark:6+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+data<<COMMIT+third(:6)+COMMIT++M644inlinefile5+data<<EOF+$file5_data+EOF++M755inlinefile6+data<<EOF+$file6_data+EOF++blob+mark:7+data<<EOF+$note1_data+EOF++blob+mark:8+data<<EOF+$note2_data+EOF++commitrefs/notes/foobar+mark:9+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+data<<COMMIT+notes(:9)+COMMIT++N:7:3+N:8:5+Ninline:6+data<<EOF+$note3_data+EOF++INPUT_END+test_expect_success\+'Q: commit notes'\+'gitfast-import<input&&+gitwhatchangednotes-test'+test_expect_success\+'Q: verify pack'\+'for p in .git/objects/pack/*.pack;do git verify-pack $p||exit;done'++commit1=$(gitrev-parsenotes-test~2)+commit2=$(gitrev-parsenotes-test^)+commit3=$(gitrev-parsenotes-test)++cat>expect<<EOF+author$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE++first(:3)+EOF+test_expect_success\+'Q: verify first commit'\+'gitcat-filecommitnotes-test~2|sed1d>actual&&+test_cmpexpectactual'++cat>expect<<EOF+parent$commit1+author$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE++second(:5)+EOF+test_expect_success\+'Q: verify second commit'\+'gitcat-filecommitnotes-test^|sed1d>actual&&+test_cmpexpectactual'++cat>expect<<EOF+parent$commit2+author$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE++third(:6)+EOF+test_expect_success\+'Q: verify third commit'\+'gitcat-filecommitnotes-test|sed1d>actual&&+test_cmpexpectactual'++cat>expect<<EOF+author$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE+committer$GIT_COMMITTER_NAME<$GIT_COMMITTER_EMAIL>$GIT_COMMITTER_DATE++notes(:9)+EOF+test_expect_success\+'Q: verify notes commit'\+'gitcat-filecommitrefs/notes/foobar|sed1d>actual&&+test_cmpexpectactual'++cat>expect.unsorted<<EOF+100644blob$commit1+100644blob$commit2+100644blob$commit3+EOF+catexpect.unsorted|sort>expect+test_expect_success\+'Q: verify notes tree'\+'gitcat-file-prefs/notes/foobar^{tree}|sed"s/ [0-9a-f]* / /">actual&&+test_cmpexpectactual'++echo"$note1_data">expect+test_expect_success\+'Q: verify note for first commit'\+'git cat-file blob refs/notes/foobar:$commit1 >actual && test_cmp expect actual'++echo"$note2_data">expect+test_expect_success\+'Q: verify note for second commit'\+'git cat-file blob refs/notes/foobar:$commit2 >actual && test_cmp expect actual'++echo"$note3_data">expect+test_expect_success\+'Q: verify note for third commit'\+'git cat-file blob refs/notes/foobar:$commit3 >actual && test_cmp expect actual'+ test_done
There's significant trailing whitespace here (in the lines between
spam, xyzzy and foo) that initially broke the test for me because I
use apply.whitespace=fix. Can you guard the whitespace if it is
really important, with something like
sed 's/#$//' > expect <<EOF
whitespace: #
EOF
Thanks!
--
Thomas Rast
trast@{inf,student}.ethz.ch
Someday we will need a way to switch off the display of notes
without resolving to oneline format.
Is there a notes specifier for the printf-like log message formatting
(--pretty=format: or --format) planned, BTW?
From: Shawn O. Pearce <hidden> Date: 2016-06-15 22:47:07
Johan Herland [off-list ref] wrote:
Introduce a 'notemodify' subcommand of the 'commit' command. This subcommand
is similar to 'filemodify', except that no mode is supplied (all notes have
mode 0644), and the path is set to the hex SHA1 of the given "comittish".
This enables fast import of note objects along with their associated commits,
since the notes can now be named using the mark references of their
corresponding commits.
The patch also includes a test case of the added functionality.
Seems sane to me.
Acked-by: Shawn O. Pearce <redacted>
Someday we will need a way to switch off the display of notes
without resolving to oneline format.
Is there a notes specifier for the printf-like log message formatting
(--pretty=format: or --format) planned, BTW?
That would probably be something like "GIT_NOTES_REF=nyanyanya git log"?
Ciao,
Dscho
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:47:07
Hi,
On Wed, 29 Jul 2009, Johan Herland wrote:
This is a relatively straightforward implementation of parsing notes
trees that use fanout directories to limit the size of individual tree
objects. This first draft uses a simple linked list for holding unparsed
subtree references (to be parsed on demand), and as such, this first
draft concentrates more on correctness than performance (AFAICS from
t3302, there is no measurable performance impact when no fanout subtrees
are present).
I know you want to have something working first and optimize then, but I
imagined that the hashmap can actually contain the entries of the partial
hashes, too. You'll need to extend the data type, of course, to be able
to say just how many digits of the SHA-1 are valid, and I guess for
consistency you'll need to pad with 0s.
BTW have you done any performance benchmarks? If so, how do they look?
Ciao,
Dscho
From: Johannes Schindelin <hidden> Date: 2016-06-15 22:47:07
Hi,
On Wed, 29 Jul 2009, Johan Herland wrote:
Creating repos with 10/100/1000/10000 commits and notes takes a lot of time.
However, using git-fast-import to do the job is a lot more efficient than
using plumbing commands to do the same.
This patch decreases the overall run-time of this test on my machine from
~3 to ~1 minutes.
From: Johan Herland <hidden> Date: 2016-06-15 22:47:07
On Wednesday 29 July 2009, Johannes Schindelin wrote:
On Wed, 29 Jul 2009, Johan Herland wrote:
quoted
This is a relatively straightforward implementation of parsing notes
trees that use fanout directories to limit the size of individual tree
objects. This first draft uses a simple linked list for holding
unparsed subtree references (to be parsed on demand), and as such, this
first draft concentrates more on correctness than performance (AFAICS
from t3302, there is no measurable performance impact when no fanout
subtrees are present).
I know you want to have something working first and optimize then, but I
imagined that the hashmap can actually contain the entries of the partial
hashes, too. You'll need to extend the data type, of course, to be able
to say just how many digits of the SHA-1 are valid, and I guess for
consistency you'll need to pad with 0s.
Thanks for the ideas. I will look into this once I have a set of
performance tests that I feel give a better picture of the notes parsing
performance.
BTW have you done any performance benchmarks? If so, how do they look?
I've just started. As I said above, t3302 (which has no fanout) is not
negatively affected by my current implementation. However this does not
say much (only that the current draft doesn't screw up completely...).
I've started making some scripts for more accurately testing the
performance of the notes parsing code at different fanout levels.
At the end of this email you will find a patch with my current state of
these scripts. (This patch is not meant to be included in the jh/notes
topic. It is only extra scripts for those interested in testing the
performance of this particular piece of code.)
METHODOLOGY
So far, there are 3 scripts:
- create_test_repo.sh: Creates a repo with 100,000 commits and one note
object per commit. Also creates 3 notes trees, each holding all
100,000 notes, but in different fanout structures:
- refs/notes/fanout0: all 100,000 notes directly inside the root tree.
- refs/notes/fanout1: notes are stored in a 2/38 structure (i.e. split
into 256 subtrees within the root tree).
- refs/notes/fanout2: notes are stored in a 2/2/36 structure (i.e. an
additional 256-fanout within each of the 256 subtrees).
- verify_correctness.sh: Verify that the "git log" output for each of the
3 notes refs is as expected. This is just to verify that the notes
parsing code is actually doing the right job.
- test_performance.sh: For each of the 3 notes refs, run "git log -n 10"
100 times, and report the time used. I feel this gives a more accurate
impression of the real-world performance of the notes parsing code,
since we:
- Test the code at several fanout levels
- Only lookup _some_ of the notes (looking up _all_ of the notes, i.e.
"git log" without -n, is clearly the worst-case scenario for any code
that loads subtree on-demand).
There is also a config script - config.sh - where you can tweak all of
the above test parameters.
PRELIMINARY RESULTS
Running the above test_performance.sh on the current state of jh/notes
give the following output on my machine:
$ ./test_performance.sh
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 0...
30.56user 2.08system 0:32.71elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+1038490minor)pagefaults 0swaps
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 1...
1.64user 0.20system 0:01.88elapsed 98%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+104883minor)pagefaults 0swaps
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 2...
0.48user 0.18system 0:00.65elapsed 101%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+94283minor)pagefaults 0swaps
...which indicates that even with my straightforward first draft based on
a simple linked list, there are huge performance wins in using one or more
fanout levels.
THE SCRIPTS
---
notes_performance/config.sh | 9 +++
notes_performance/create_test_repo.sh | 90 +++++++++++++++++++++++++++++++
notes_performance/test_performance.sh | 31 +++++++++++
notes_performance/verify_correctness.sh | 32 +++++++++++
4 files changed, 162 insertions(+), 0 deletions(-)
create mode 100644 notes_performance/config.sh
create mode 100755 notes_performance/create_test_repo.sh
create mode 100755 notes_performance/test_performance.sh
create mode 100755 notes_performance/verify_correctness.sh
@@ -0,0 +1,90 @@+#!/bin/sh++source./config.sh++# Create a test repo with $num_commits commits, and one note object per commit.+# The repo has the following refs:+# - refs/heads/master pointing to the last commit+# - refs/notes/fanout0 referencing all notes with no fanout+# - refs/notes/fanout1 referencing all notes with 2/38 fanout+# - refs/notes/fanout2 referencing all notes with 2/2/36 fanout++if[-d$repo_dir];then+echo"Skipping repo creation, $repo_dir already exists"+exit1+fi++mkdir$repo_dir&&+cd$repo_dir&&+$GITinit&&+nr=0&&+echo"Creating $num_commits commits..."1>&2&&+while[$nr-lt$num_commits];do+nr=$(($nr+1))&&+cat<<INPUT_END+commitrefs/heads/master+committerFooBar<foobar@example.com>1234567890+0000+data<<COMMIT+commit#$nr+COMMIT++M644inlinefile+data<<EOF+fileincommit#$nr+EOF++INPUT_END++done|+$GITfast-import--quiet&&+echo"done"1>&2&&+(+echo"Creating $num_commits notes..."1>&2&&+nr=0&&+while[$nr-lt$num_commits];do+nr=$(($nr+1))&&+cat<<INPUT_END+blob+mark:$nr+data<<EOF+noteforcommit#$nr+EOF++INPUT_END++done&&+echo"done"1>&2&&+forfanout_levelsin012;do+notes_ref="refs/notes/fanout$fanout_levels"&&+echo"Creating notes tree with fanout level $fanout_levels..."1>&2&&+cat<<INPUT_END&&+commit$notes_ref+committerFooBar<foobar@example.com>1234567890+0000+data<<COMMIT+noteswithfanoutlevel$fanout_levels+COMMIT++INPUT_END++nr=$num_commits&&+$GITrev-listrefs/heads/master|+whilereadsha1;do+case$fanout_levelsin+0)+note_path=$sha1+;;+1)+note_path="${sha1:0:2}/${sha1:2}"+;;+2)+note_path="${sha1:0:2}/${sha1:2:2}/${sha1:4}"+;;+esac&&+echo"M 100644 :$nr$note_path"&&+nr=$(($nr-1))+done&&+echo"done"1>&2+done+)|+$GITfast-import--quiet&&+$GITgc
@@ -0,0 +1,31 @@+#!/bin/sh++source./config.sh++# Test "git log -n $log_length" for each of the 3 notes refs+# - refs/notes/fanout0+# - refs/notes/fanout1+# - refs/notes/fanout2++if[!-d$repo_dir];then+echo"Cannot test performance, $repo_dir missing"+exit1+fi++cd$repo_dir&&+cat>time_notes<<EOF&&+i=0&&+while[\$i-lt$log_reps];do+$GITlog-n$log_lengthrefs/heads/master>/dev/null&&+i=\$((\$i+1))+done++EOF++forfanout_levelsin012;do+echo"Timing $log_reps reps of 'git log -n $log_length refs/heads/master >/dev/null' at fanout level $fanout_levels..."&&+notes_ref="refs/notes/fanout$fanout_levels"&&+$GITconfigcore.notesRef$notes_ref&&+/usr/bin/timeshtime_notes&&+echo+done
Someday we will need a way to switch off the display of notes
without resolving to oneline format.
Is there a notes specifier for the printf-like log message formatting
(--pretty=format: or --format) planned, BTW?
That would probably be something like "GIT_NOTES_REF=nyanyanya git log"?
Yes, that works, although I suspect some users will prefer a command-line
argument instead.
Nonetheless, I think it makes sense to add a notes specifier to be used in
--pretty/--format.
I'll try to remember to look into this later, but I'll be grateful if
someone gets to it before me.
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
There's significant trailing whitespace here (in the lines between
spam, xyzzy and foo) that initially broke the test for me because I
use apply.whitespace=fix. Can you guard the whitespace if it is
really important, with something like
sed 's/#$//' > expect <<EOF
whitespace: #
EOF
Thanks! Will be fixed in the next iteration of this topic.
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
Someday we will need a way to switch off the display of notes
without resolving to oneline format.
Is there a notes specifier for the printf-like log message formatting
(--pretty=format: or --format) planned, BTW?
That would probably be something like "GIT_NOTES_REF=nyanyanya git log"?
Yes, that works, although I suspect some users will prefer a command-line
argument instead.
Nonetheless, I think it makes sense to add a notes specifier to be used in
--pretty/--format.
I'll try to remember to look into this later, but I'll be grateful if
someone gets to it before me.
Probably you will not want to show the "\nNotes:" prefix, and also not
indent the string, but that is something you could make conditional upon a
flag to get_commit_notes(). But this should get you started:
-- snipsnap --
@@ -123,6 +123,7 @@ The placeholders are: - '%s': subject - '%f': sanitized subject line, suitable for a filename - '%b': body+- '%N': commit notes - '%Cred': switch color to red - '%Cgreen': switch color to green - '%Cblue': switch color to blue
@@ -690,6 +690,10 @@ static size_t format_commit_item(struct strbuf *sb, const char *placeholder,case'd':format_decoration(sb,commit);return1;+case'N':+get_commit_notes(commit,sb,git_log_output_encoding?+git_log_output_encoding:git_commit_encoding);+return1;}/* For the rest we have to parse the commit header. */
I thought you wanted to use the note code to handle the name
formatting here?
Yes, I do, as soon as the notes code knows how to format/write note names
(which I plan to tackle after nailing the _reading_ part of the code).
Have fun! :)
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
From: Johan Herland <hidden> Date: 2016-06-15 22:47:08
This patch stores note entries and unexpanded fanout subtree entries in a
customized 256-tree structure.
Initial performance numbers are encouraging:
$ ./test_performance.sh
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 0...
14.92user 4.84system 0:20.39elapsed 96%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+2164780minor)pagefaults 0swaps
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 1...
0.71user 0.32system 0:01.06elapsed 97%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+154090minor)pagefaults 0swaps
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 2...
0.44user 0.18system 0:00.63elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+94183minor)pagefaults 0swaps
This is pretty much twice as fast as the existing version (which uses a hash map
for notes, and a linked list for unexpanded fanout subtrees).
Signed-off-by: Johan Herland <redacted>
---
Hi,
Just a quick update before I leave for a week of vacation (with spotty
Internet access, at best).
I've been thinking about various data structures for the notes code for
the last couple of days, and here is a quick first draft of the idea that
I found most promising: Storing both notes and unexpanded subtrees as leaf
nodes in a customized 256-tree structure. The initial performance numbers
look very promising (~twice as fast as the previous implementation at
fanout levels 0 and 1), and there are still probably several optimization
that can be done (an obvious example is reducing malloc pressure by
memory pooling leaf_node objects).
AFAICS, this implementation is semantically equivalent the previous code
(longer prefixes are preferred, no merging of notes, multiple/nested
fanout levels are allowed, etc.).
Have fun! :)
...Johan
notes.c | 325 ++++++++++++++++++++++++++++++++++-----------------------------
1 files changed, 174 insertions(+), 151 deletions(-)
@@ -6,79 +6,165 @@#include"strbuf.h"#include"tree-walk.h"-structentry{-unsignedcharcommit_sha1[20];-unsignedcharnotes_sha1[20];+/*+*Useanon-balancingsimple256-treestructurewithstructint_nodeas+*internalnodes,andstructleaf_nodeasleafnodes.Eachint_nodehasa+*256-arrayofpointerstoitschildren+*Thebottom2bitsofeachpointerisusedtoidentifythepointertype+*-ptr&3==0-NULLpointer,assert(ptr==NULL)+*-ptr&3==1-pointertonextinternalnode-casttostructint_node*+*-ptr&3==2-pointertonoteentry-casttostructleaf_node*+*-ptr&3==3-pointertosubtreeentry-casttostructleaf_node*+*+*Therootnodeisalwaysaninlinestructint_node.+*Anarray_entryalwaysstartsoutwithallpointerssettoNULL.+*+*Toaddaleaf_node:+*1.Startattherootnode,withn=0+*2.Usethenthbyteofthekeyasanindexintoa:+*-IfNULL,storethetweakedpointerdirectlyintoa[n]+*-Ifanint_node,recurseintothatnodeandincrementn+*-Ifaleaf_node:+*1.Checkifthey'reequal,andhandlethat(abort?overwrite?)+*2.Createanewint_node,andstorebothleaf_nodesthere+*3.Storethenewint_nodeintoa[n].+*+*Tofindaleaf_node:+*1.Startattherootnode,withn=0+*2.Usethenthbyteofthekeyasanindexintoa:+*-Ifanint_node,recurseintothatnodeandincrementn+*-Ifaleaf_nodewithmatchingkey,returnleaf_node(assertnoteentry)+*-Ifamatchingsubtreeentry,unpackthatsubtreeentry(andremoveit);+*restartsearchatthecurrentlevel.+*-Otherwise,weendupataNULLpointer,oranon-matchingleaf_node.+*Backtrackoutoftherecursion,onelevelatatimeandchecka[0]:+*-Ifa[0]atthecurrentlevelisamatchingsubtreeentry,unpackthat+*subtreeentry(andremoveit);restartsearchatthecurrentlevel.+*/+structint_node{+void*a[256];};-structhash_map{-structentry*entries;-off_tcount,size;+/*+*Leafnodescomeintwovariants,noteentriesandsubtreeentries,+*distinguishedbytheLSboftheleafnodepointer(seeabove).+*Asanoteentry,thekeyistheSHA1ofthereferencedcommit,andthevalue+*istheSHA1ofthenoteobject.+*Asasubtreeentry,thekeyistheprefixSHA1(w/trailingNULs)ofthe+*referencedcommit,includingtheprefixlengthinthelastbyteofthekey.+*ThevalueistheSHA1ofthetreeobjectcontainingthenotessubtree.+*/+structleaf_node{+unsignedcharkey_sha1[20];+unsignedcharval_sha1[20];};-structsubtree_entry{-/*-*SHA1prefixisstoredinthefirst19bytes(w/trailingNULbytes);-*lengthofSHA1prefixisstoredinthelastbyte-*/-unsignedcharsha1_prefix_w_len[20];-unsignedcharsubtree_sha1[20];-structsubtree_entry*next;-};+#define PTR_TYPE_NULL 0+#define PTR_TYPE_INTERNAL 1+#define PTR_TYPE_NOTE 2+#define PTR_TYPE_SUBTREE 3-staticintinitialized;-staticstructhash_maphash_map;-staticstructsubtree_entry*subtree_list;+#define GET_PTR_TYPE(ptr) ((uintptr_t) (ptr) & 3)+#define CLR_PTR_TYPE(ptr) ((void *) ((uintptr_t) (ptr) & ~3))+#define SET_PTR_TYPE(ptr, type) ((void *) ((uintptr_t) (ptr) | (type)))-staticinthash_index(structhash_map*map,constunsignedchar*sha1)-{-inti=((*(unsignedint*)sha1)%map->size);+#define MATCHING_SUBTREE(key_sha1, subtree_sha1) \+(!memcmp(key_sha1,subtree_sha1,subtree_sha1[19]))-for(;;){-unsignedchar*current=map->entries[i].commit_sha1;+staticstructint_noderoot_node;-if(!hashcmp(sha1,current))-returni;+staticintinitialized;-if(is_null_sha1(current))-return-1-i;-if(++i==map->size)-i=0;-}-}+staticvoidload_subtree(structleaf_node*subtree,structint_node*node,+unsignedintn);-staticvoidadd_entry(constunsignedchar*commit_sha1,-constunsignedchar*notes_sha1)+staticstructleaf_node*note_tree_find(structint_node*tree,unsignedcharn,+constunsignedchar*key_sha1){-intindex;--if(hash_map.count+1>hash_map.size>>1){-inti,old_size=hash_map.size;-structentry*old=hash_map.entries;--hash_map.size=old_size?old_size<<1:64;-hash_map.entries=(structentry*)-xcalloc(sizeof(structentry),hash_map.size);--for(i=0;i<old_size;i++)-if(!is_null_sha1(old[i].commit_sha1)){-index=-1-hash_index(&hash_map,-old[i].commit_sha1);-memcpy(hash_map.entries+index,old+i,-sizeof(structentry));-}-free(old);+structleaf_node*l;+unsignedchari=key_sha1[n];+void*p=tree->a[i];++switch(GET_PTR_TYPE(p)){+casePTR_TYPE_INTERNAL:+l=note_tree_find(CLR_PTR_TYPE(p),n+1,key_sha1);+if(l)+returnl;+break;+casePTR_TYPE_NOTE:+l=(structleaf_node*)CLR_PTR_TYPE(p);+if(!hashcmp(key_sha1,l->key_sha1))+returnl;/* return note object matching given key */+break;+casePTR_TYPE_SUBTREE:+l=(structleaf_node*)CLR_PTR_TYPE(p);+if(MATCHING_SUBTREE(key_sha1,l->key_sha1)){+/* unpack tree and resume search */+tree->a[i]=NULL;+load_subtree(l,tree,n);+free(l);+returnnote_tree_find(tree,n,key_sha1);+}+break;+casePTR_TYPE_NULL:+default:+assert(!p);+break;}-index=hash_index(&hash_map,commit_sha1);-if(index<0){-index=-1-index;-hash_map.count++;+/*+*Didnotfindkeyatthis(oranylower)level.+*Checkifthere'samatchingsubtreeentryintree->a[0].+*Ifso,unpacktreeandresumesearch.+*/+p=tree->a[0];+if(GET_PTR_TYPE(p)!=PTR_TYPE_SUBTREE)+returnNULL;+l=(structleaf_node*)CLR_PTR_TYPE(p);+if(MATCHING_SUBTREE(key_sha1,l->key_sha1)){+/* unpack tree and resume search */+tree->a[0]=NULL;+load_subtree(l,tree,n);+free(l);+returnnote_tree_find(tree,n,key_sha1);}+returnNULL;+}-hashcpy(hash_map.entries[index].commit_sha1,commit_sha1);-hashcpy(hash_map.entries[index].notes_sha1,notes_sha1);+staticintnote_tree_insert(structint_node*tree,unsignedcharn,+conststructleaf_node*entry,unsignedchartype)+{+structint_node*new_node;+conststructleaf_node*l;+intret;+unsignedchari=entry->key_sha1[n];+void*p=tree->a[i];+assert(GET_PTR_TYPE(entry)==PTR_TYPE_NULL);+switch(GET_PTR_TYPE(p)){+casePTR_TYPE_NULL:+assert(!p);+tree->a[i]=SET_PTR_TYPE(entry,type);+return0;+casePTR_TYPE_INTERNAL:+returnnote_tree_insert(CLR_PTR_TYPE(p),n+1,entry,type);+default:+assert(GET_PTR_TYPE(p)==PTR_TYPE_NOTE||+GET_PTR_TYPE(p)==PTR_TYPE_SUBTREE);+l=(conststructleaf_node*)CLR_PTR_TYPE(p);+if(!hashcmp(entry->key_sha1,l->key_sha1))+return-1;/* abort insert on matching key */+new_node=(structint_node*)+xcalloc(sizeof(structint_node),1);+ret=note_tree_insert(new_node,n+1,+CLR_PTR_TYPE(p),GET_PTR_TYPE(p));+if(ret){+free(new_node);+return-1;+}+tree->a[i]=SET_PTR_TYPE(new_node,PTR_TYPE_INTERNAL);+returnnote_tree_insert(new_node,n+1,entry,type);+}}/*
@@ -110,22 +196,23 @@ static int get_sha1_hex_segment(const char *hex, unsigned int hex_len,returnlen;}-staticvoidload_subtree(structsubtree_entry*se)+staticvoidload_subtree(structleaf_node*subtree,structint_node*node,+unsignedintn){unsignedcharcommit_sha1[20];unsignedintprefix_len;void*buf;structtree_descdesc;structname_entryentry;-structsubtree_entry*tmp_list=NULL,*tmp_last=NULL;-buf=fill_tree_descriptor(&desc,se->subtree_sha1);+buf=fill_tree_descriptor(&desc,subtree->val_sha1);if(!buf)die("Could not read %s for notes-index",-sha1_to_hex(se->subtree_sha1));+sha1_to_hex(subtree->val_sha1));-prefix_len=se->sha1_prefix_w_len[19];-memcpy(commit_sha1,se->sha1_prefix_w_len,prefix_len);+prefix_len=subtree->key_sha1[19];+assert(prefix_len>=n);+memcpy(commit_sha1,subtree->key_sha1,prefix_len);while(tree_entry(&desc,&entry)){intlen=get_sha1_hex_segment(entry.path,strlen(entry.path),commit_sha1+prefix_len,20-prefix_len);
@@ -133,111 +220,47 @@ static void load_subtree(struct subtree_entry *se)continue;/* entry.path is not a SHA1 sum. Skip */len+=prefix_len;-/* If commit SHA1 is complete, assume note object */-if(len==20)-add_entry(commit_sha1,entry.sha1);-/* If commit SHA1 is incomplete, assume note subtree */-elseif(len<20&&entry.mode==S_IFDIR){-structsubtree_entry*n=(structsubtree_entry*)-xcalloc(sizeof(structsubtree_entry),1);-hashcpy(n->sha1_prefix_w_len,commit_sha1);-n->sha1_prefix_w_len[19]=(unsignedchar)len;-hashcpy(n->subtree_sha1,entry.sha1);--if(!tmp_list){-tmp_list=n;-tmp_last=n;-}-else{-assert(!tmp_last->next);-assert(hashcmp(n->sha1_prefix_w_len,-tmp_last->sha1_prefix_w_len)>0);-tmp_last->next=n;-tmp_last=n;+/*+*IfcommitSHA1iscomplete(len==20),assumenoteobject+*IfcommitSHA1isincomplete(len<20),assumenotesubtree+*/+if(len<=20){+unsignedchartype=PTR_TYPE_NOTE;+structleaf_node*l=(structleaf_node*)+xcalloc(sizeof(structleaf_node),1);+hashcpy(l->key_sha1,commit_sha1);+hashcpy(l->val_sha1,entry.sha1);+if(len<20){+l->key_sha1[19]=(unsignedchar)len;+type=PTR_TYPE_SUBTREE;}+assert(!note_tree_insert(node,n,l,type));}}free(buf);-if(tmp_list){-/* insert tmp_list immediately after se */-assert(hashcmp(tmp_list->sha1_prefix_w_len,-se->sha1_prefix_w_len)>0);-if(se->next){-assert(hashcmp(se->next->sha1_prefix_w_len,-tmp_last->sha1_prefix_w_len)>0);-tmp_last->next=se->next;-}-se->next=tmp_list;-}}-staticvoidinitialize_hash_map(constchar*notes_ref_name)+staticvoidinitialize_notes(constchar*notes_ref_name){unsignedcharsha1[20],commit_sha1[20];unsignedmode;-structsubtree_entryroot_tree;+structleaf_noderoot_tree;if(!notes_ref_name||read_ref(notes_ref_name,commit_sha1)||get_tree_entry(commit_sha1,"",sha1,&mode))return;-hashclr(root_tree.sha1_prefix_w_len);-hashcpy(root_tree.subtree_sha1,sha1);-root_tree.next=NULL;-load_subtree(&root_tree);-subtree_list=root_tree.next;-}--/*-*ComparethegivencommitSHA1againstthegivensubtreeentry.-*Return-1ifthecommitSHA1cannotexistwithinthegivensubtree,orany-*subtreefollowingit.-*Return0ifthecommitSHA1_may_existwithinthegivensubtree.-*Return1ifthecommitSHA1cannotexistwithinthegivensubtree,butmay-*existwithinasubtreefollowingit.-*/-staticintcommit_subtree_cmp(constunsignedchar*commit_sha1,-conststructsubtree_entry*entry)-{-unsignedintprefix_len=entry->sha1_prefix_w_len[19];-returnmemcmp(commit_sha1,entry->sha1_prefix_w_len,prefix_len);-}--staticstructsubtree_entry*lookup_subtree(constunsignedchar*commit_sha1)-{-structsubtree_entry*found=NULL,*cur=subtree_list;-while(cur){-intcmp=commit_subtree_cmp(commit_sha1,cur);-if(!cmp)-found=cur;-if(cmp<0)-break;-cur=cur->next;-}-returnfound;+hashclr(root_tree.key_sha1);+hashcpy(root_tree.val_sha1,sha1);+load_subtree(&root_tree,&root_node,0);}staticunsignedchar*lookup_notes(constunsignedchar*commit_sha1){-intindex;-structsubtree_entry*subtree;--/* First, try to find the commit SHA1 directly in hash map */-index=hash_map.size?hash_index(&hash_map,commit_sha1):-1;-if(index>=0)-returnhash_map.entries[index].notes_sha1;--/* Next, try finding a subtree that may contain the commit SHA1 */-subtree=lookup_subtree(commit_sha1);--/* Give up if no subtree found, or if subtree is already loaded */-if(!subtree||is_null_sha1(subtree->subtree_sha1))-returnNULL;--/* Load subtree into hash_map, and retry lookup recursively */-load_subtree(subtree);-hashclr(subtree->subtree_sha1);-returnlookup_notes(commit_sha1);+structleaf_node*found=note_tree_find(&root_node,0,commit_sha1);+if(found)+returnfound->val_sha1;+returnNULL;}voidget_commit_notes(conststructcommit*commit,structstrbuf*sb,
From: Johan Herland <hidden> Date: 2016-06-15 22:47:14
This is a first draft at implementing Dscho's idea of improving note parser
performance by storing the subtree entries in the same hash map as the note
entries.
In order to tell subtree entries and note entries apart, another member has
been added to struct entry: unsigned char commit_sha1_len. This member
stores the number of "valid" bytes in the commit_sha1 member, meaning that
a value of 1-19 indicates a subtree entry, and a value of 20 indicates a
note entry. There are two more special values for this new member as well:
- Since a null SHA1 is also a valid subtree entry (e.g. the "00/*" subtree
in a 2/38 fanout scheme), we can no longer use is_null_sha1() to identify
unused entries in the hash map. Instead, a value of 0 in the new
commit_sha1_len member indicates that this is entry is null/unused.
There is one exception to this rule: the root notes tree which is just a
special case of a subtree entry with 0 "valid" bytes in the commit_sha1
member. However, this exception is acceptable, as the root entry is never
stored in the hash map (the hash map is initialized by unpacking this
root tree entry).
- The second special commit_sha1_len value is 255, and is used to indicate
a subtree entry that has already been unpacked, and should therefore be
removed from the hash map. It is expensive and non-trivial to _delete_
an entry in the hash map, and we therefore use this special value to
_ignore_ the entry. If/when the hash map is grown/reallocated, we simply
avoid bringing the unpacked subtree entries into the new hash map.
$ ./test_performance.sh
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 0...
21.80user 2.15system 0:24.12elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+1051886minor)pagefaults 0swaps
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 1...
1.24user 0.24system 0:01.49elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+120478minor)pagefaults 0swaps
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 2...
0.73user 0.20system 0:00.95elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+105782minor)pagefaults 0swaps
Signed-off-by: Johan Herland <redacted>
---
On Wednesday 29 July 2009, Johannes Schindelin wrote:
I know you want to have something working first and optimize then, but I
imagined that the hashmap can actually contain the entries of the partial
hashes, too. You'll need to extend the data type, of course, to be able
to say just how many digits of the SHA-1 are valid, and I guess for
consistency you'll need to pad with 0s.
Ok. Here's a draft implementation of your proposal to store the subtree
entries in the same hash map as the note entries.
This code is somewhat faster than my first subtree-in-linked-list draft,
but considerably slower than my 256-tree proposal:
fanout level 0 fanout level 1 fanout level 2
subtree-in-ll 32.71s 1.88s 0.65s
all-in-hash-map 24.12s 1.49s 0.95s
all-in-256-tree 20.39s 1.06s 0.63s
I'm not sure how closely my implementation follows your vision, so please
suggest improvements, fixes, etc.
Have fun! :)
...Johan
notes.c | 155 +++++++++++++++++++++++++--------------------------------------
1 files changed, 62 insertions(+), 93 deletions(-)
@@ -110,22 +119,26 @@ static int get_sha1_hex_segment(const char *hex, unsigned int hex_len,returnlen;}-staticvoidload_subtree(structsubtree_entry*se)+staticvoidload_subtree(structentry*subtree){unsignedcharcommit_sha1[20];unsignedintprefix_len;void*buf;structtree_descdesc;structname_entryentry;-structsubtree_entry*tmp_list=NULL,*tmp_last=NULL;-buf=fill_tree_descriptor(&desc,se->subtree_sha1);+buf=fill_tree_descriptor(&desc,subtree->notes_sha1);if(!buf)die("Could not read %s for notes-index",-sha1_to_hex(se->subtree_sha1));+sha1_to_hex(subtree->notes_sha1));++prefix_len=subtree->commit_sha1_len;+assert(prefix_len<20);+memcpy(commit_sha1,subtree->commit_sha1,prefix_len);++/* Invalidate this subtree from further consideration */+subtree->commit_sha1_len=255;-prefix_len=se->sha1_prefix_w_len[19];-memcpy(commit_sha1,se->sha1_prefix_w_len,prefix_len);while(tree_entry(&desc,&entry)){intlen=get_sha1_hex_segment(entry.path,strlen(entry.path),commit_sha1+prefix_len,20-prefix_len);
@@ -133,110 +146,66 @@ static void load_subtree(struct subtree_entry *se)continue;/* entry.path is not a SHA1 sum. Skip */len+=prefix_len;-/* If commit SHA1 is complete, assume note object */-if(len==20)-add_entry(commit_sha1,entry.sha1);-/* If commit SHA1 is incomplete, assume note subtree */-elseif(len<20&&entry.mode==S_IFDIR){-structsubtree_entry*n=(structsubtree_entry*)-xcalloc(sizeof(structsubtree_entry),1);-hashcpy(n->sha1_prefix_w_len,commit_sha1);-n->sha1_prefix_w_len[19]=(unsignedchar)len;-hashcpy(n->subtree_sha1,entry.sha1);--if(!tmp_list){-tmp_list=n;-tmp_last=n;-}-else{-assert(!tmp_last->next);-assert(hashcmp(n->sha1_prefix_w_len,-tmp_last->sha1_prefix_w_len)>0);-tmp_last->next=n;-tmp_last=n;-}-}+if(len==20||(len<20&&entry.mode==S_IFDIR))+add_entry(commit_sha1,entry.sha1,len);}free(buf);-if(tmp_list){-/* insert tmp_list immediately after se */-assert(hashcmp(tmp_list->sha1_prefix_w_len,-se->sha1_prefix_w_len)>0);-if(se->next){-assert(hashcmp(se->next->sha1_prefix_w_len,-tmp_last->sha1_prefix_w_len)>0);-tmp_last->next=se->next;-}-se->next=tmp_list;-}}staticvoidinitialize_hash_map(constchar*notes_ref_name){unsignedcharsha1[20],commit_sha1[20];unsignedmode;-structsubtree_entryroot_tree;+structentryroot_tree;if(!notes_ref_name||read_ref(notes_ref_name,commit_sha1)||get_tree_entry(commit_sha1,"",sha1,&mode))return;-hashclr(root_tree.sha1_prefix_w_len);-hashcpy(root_tree.subtree_sha1,sha1);-root_tree.next=NULL;+hashclr(root_tree.commit_sha1);+hashcpy(root_tree.notes_sha1,sha1);+root_tree.commit_sha1_len=0;load_subtree(&root_tree);-subtree_list=root_tree.next;}-/*-*ComparethegivencommitSHA1againstthegivensubtreeentry.-*Return-1ifthecommitSHA1cannotexistwithinthegivensubtree,orany-*subtreefollowingit.-*Return0ifthecommitSHA1_may_existwithinthegivensubtree.-*Return1ifthecommitSHA1cannotexistwithinthegivensubtree,butmay-*existwithinasubtreefollowingit.-*/-staticintcommit_subtree_cmp(constunsignedchar*commit_sha1,-conststructsubtree_entry*entry)+staticstructentry*lookup_subtree(constunsignedchar*commit_sha1){-unsignedintprefix_len=entry->sha1_prefix_w_len[19];-returnmemcmp(commit_sha1,entry->sha1_prefix_w_len,prefix_len);-}+unsignedcharprefix_sha1[20];+unsignedchari;+intindex;-staticstructsubtree_entry*lookup_subtree(constunsignedchar*commit_sha1)-{-structsubtree_entry*found=NULL,*cur=subtree_list;-while(cur){-intcmp=commit_subtree_cmp(commit_sha1,cur);-if(!cmp)-found=cur;-if(cmp<0)-break;-cur=cur->next;+hashcpy(prefix_sha1,commit_sha1);+for(i=19;i;--i){+prefix_sha1[i]=0;+index=hash_index(&hash_map,prefix_sha1);+if(index>=0&&hash_map.entries[index].commit_sha1_len==i)+return&(hash_map.entries[index]);}-returnfound;+returnNULL;}staticunsignedchar*lookup_notes(constunsignedchar*commit_sha1){intindex;-structsubtree_entry*subtree;+structentry*subtree;++if(!hash_map.size)+returnNULL;/* First, try to find the commit SHA1 directly in hash map */-index=hash_map.size?hash_index(&hash_map,commit_sha1):-1;+index=hash_index(&hash_map,commit_sha1);if(index>=0)returnhash_map.entries[index].notes_sha1;/* Next, try finding a subtree that may contain the commit SHA1 */subtree=lookup_subtree(commit_sha1);-/* Give up if no subtree found, or if subtree is already loaded */-if(!subtree||is_null_sha1(subtree->subtree_sha1))+/* Give up if no subtree found */+if(!subtree)returnNULL;/* Load subtree into hash_map, and retry lookup recursively */load_subtree(subtree);-hashclr(subtree->subtree_sha1);returnlookup_notes(commit_sha1);}
From: Johan Herland <hidden> Date: 2016-06-15 22:47:19
The 256-tree structure is considerably faster than storing all entries in a
hash_map. Also, the memory consumption of the 256-tree structure is lower
than the hash_map, provided that you're only loading a few notes from a
"properly fanned-out" notes tree (i.e. 100000 notes in a 2/2/36 structure).
However, in the worst case (loading all 100000 notes), the memory usage of
the 256-tree structure (62.64 MB) is significantly worse than the hash_map
approach (10.25 MB).
This patch modifies the 256-tree structure into a 16-tree structure. This
significantly improves the memory situation. The result uses less memory
than both the 256-tree structure, and the hash_map approach, with a worst
case usage of 8.54 MB. Additionally, it seems to slightly improve the
runtime performance as well (probably because of the improved memory usage).
In conclusion, this is faster and smaller than all the previous drafts.
$ ./test_performance.sh
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 0...
15.05user 1.44system 0:16.59elapsed 99%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+782490minor)pagefaults 0swaps
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 1...
0.68user 0.17system 0:00.87elapsed 98%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+99585minor)pagefaults 0swaps
Timing 100 reps of 'git log -n 10 refs/heads/master >/dev/null' at fanout level 2...
0.41user 0.17system 0:00.61elapsed 97%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+94084minor)pagefaults 0swaps
Signed-off-by: Johan Herland <redacted>
---
Hi,
This patch goes on top of the 256-tree RFC I sent earlier.
If nobody suggests further improvements, this patch will be
included in the next iteration of the jh/notes topic.
Have fun! :)
...Johan
notes.c | 26 ++++++++++++++------------
1 files changed, 14 insertions(+), 12 deletions(-)
From: Alex Riesen <hidden> Date: 2016-06-15 22:47:19
On Wed, Aug 26, 2009 at 12:31, Johan Herland[off-list ref] wrote:
The 256-tree structure is considerably faster than storing all entries in a
This part is confusing. Was 256-tree better (as in "faster") then?
hash_map. Also, the memory consumption of the 256-tree structure is lower
than the hash_map, provided that you're only loading a few notes from a
"properly fanned-out" notes tree (i.e. 100000 notes in a 2/2/36 structure).
However, in the worst case (loading all 100000 notes), the memory usage of
the 256-tree structure (62.64 MB) is significantly worse than the hash_map
approach (10.25 MB).
This patch modifies the 256-tree structure into a 16-tree structure. This
significantly improves the memory situation. The result uses less memory
than both the 256-tree structure, and the hash_map approach, with a worst
case usage of 8.54 MB. Additionally, it seems to slightly improve the
runtime performance as well (probably because of the improved memory usage).
From: Johan Herland <hidden> Date: 2016-06-15 22:47:19
On Wednesday 26 August 2009, Alex Riesen wrote:
On Wed, Aug 26, 2009 at 12:31, Johan Herland[off-list ref] wrote:
quoted
The 256-tree structure is considerably faster than storing all
entries in a
This part is confusing. Was 256-tree better (as in "faster") then?
256-tree is faster than the everything-in-hash_map draft.
16-tree is slightly faster than 256-tree
256-tree uses more memory (in the worst case) that the
everything-in-hash-map draft.
16-tree uses less memory than both.
Makes sense?
...Johan
--
Johan Herland, [off-list ref]
www.herland.net
From: Alex Riesen <hidden> Date: 2016-06-15 22:47:19
On Wed, Aug 26, 2009 at 14:56, Johan Herland[off-list ref] wrote:
On Wednesday 26 August 2009, Alex Riesen wrote:
quoted
On Wed, Aug 26, 2009 at 12:31, Johan Herland[off-list ref] wrote:
quoted
The 256-tree structure is considerably faster than storing all
entries in a
This part is confusing. Was 256-tree better (as in "faster") then?
256-tree is faster than the everything-in-hash_map draft.
16-tree is slightly faster than 256-tree
256-tree uses more memory (in the worst case) that the
everything-in-hash-map draft.
16-tree uses less memory than both.
Makes sense?
Oh, it does, it is just confusingly presented. How about:
The 16-tree is both faster and has lower footprint then 256-tree
code, which in its turn is noticably faster and smaller then existing
hash_map implementation. ...
From: Andreas Ericsson <hidden> Date: 2016-06-15 22:47:19
Alex Riesen wrote:
On Wed, Aug 26, 2009 at 14:56, Johan Herland[off-list ref] wrote:
quoted
On Wednesday 26 August 2009, Alex Riesen wrote:
quoted
On Wed, Aug 26, 2009 at 12:31, Johan Herland[off-list ref] wrote:
quoted
The 256-tree structure is considerably faster than storing all
entries in a
This part is confusing. Was 256-tree better (as in "faster") then?
256-tree is faster than the everything-in-hash_map draft.
16-tree is slightly faster than 256-tree
256-tree uses more memory (in the worst case) that the
everything-in-hash-map draft.
16-tree uses less memory than both.
Makes sense?
Oh, it does, it is just confusingly presented. How about:
The 16-tree is both faster and has lower footprint then 256-tree
code, which in its turn is noticably faster and smaller then existing
hash_map implementation. ...
If it's to be squashed in, why mention the 256-tree at all (except
for possibly as something to compare with at the end)?
If it goes on top, why mention the hash_map at all?
--
Andreas Ericsson andreas.ericsson@op5.se
OP5 AB www.op5.se
Tel: +46 8-230225 Fax: +46 8-230231
Considering the successes of the wars on alcohol, poverty, drugs and
terror, I think we should give some serious thought to declaring war
on peace.
From: Johan Herland <hidden> Date: 2016-06-15 22:47:19
On Wednesday 26 August 2009, Andreas Ericsson wrote:
Alex Riesen wrote:
quoted
On Wed, Aug 26, 2009 at 14:56, Johan Herland[off-list ref]
wrote:
quoted
quoted
On Wednesday 26 August 2009, Alex Riesen wrote:
quoted
On Wed, Aug 26, 2009 at 12:31, Johan Herland[off-list ref]
wrote:
quoted
quoted
quoted
quoted
The 256-tree structure is considerably faster than storing all
entries in a
This part is confusing. Was 256-tree better (as in "faster")
then?
256-tree is faster than the everything-in-hash_map draft.
16-tree is slightly faster than 256-tree
256-tree uses more memory (in the worst case) that the
everything-in-hash-map draft.
16-tree uses less memory than both.
Makes sense?
Oh, it does, it is just confusingly presented. How about:
The 16-tree is both faster and has lower footprint then 256-tree
code, which in its turn is noticably faster and smaller then
existing hash_map implementation. ...
If it's to be squashed in, why mention the 256-tree at all (except
for possibly as something to compare with at the end)?
If it goes on top, why mention the hash_map at all?
Ah. Sorry for the confusion. These patches are not meant to standalone
patches in the jh/notes series. They just compare various solutions to
the problem of parsing a notes tree structure with fanout in an
efficient manner.
The next iteration of the jh/notes series will include the preferred
solution (16-tree unless something better shows up), _without_ talking
about the differences between alternative solutions. As such the
hash_map and 256-tree will not be mentioned at all.
...Johan
--
Johan Herland, [off-list ref]
www.herland.net