Fossil Forum
Post: Suggestion: check only the named files in `fossil commit FILE...`
Problem
When files are named on the command line, fossil commit still checks every file in the checkout, so a one-file commit gets slower as the checkout grows.
Measurements
One-file commits (fossil commit --nosync -m x FILE) with repo-cksum off, trunk 76742655f0, Windows 11, NTFS, three runs each:
| files in checkout | trunk | trunk + patch | trunk + patch + --delta |
|---|---|---|---|
| 2,000 | 105 ms | 87 ms | 86 ms |
| 20,000 | 430 ms | 212 ms | 160 ms |
| 58,000 | 1,110 ms | 510 ms | 335 ms |
I use Fossil as the store of an automatic file-history tool that learns from the file system which files changed and commits only those, 100 to 200 times per hour. For this use the cost of a commit should follow the size of the change, not the size of the checkout.
Cause
commit_cmd() calls unsaved_changes() before select_commit_files(), and unsaved_changes() runs vfile_check_signature() over the whole checkout. Only the selected files are stored (is_selected(id)), and the other files keep their vfile.mrid in the new manifest, so checking them does not change the result.
Suggestion
Would you consider checking only the named files when files are named? The patch below does this in two places:
commit_cmd()callsselect_commit_files()beforeunsaved_changes()(the block is moved, not changed);vfile_check_signature()addsAND is_selected(id)to its query.is_selected()is true for every file when no files are named, sostatus,changesand a plaincommitbehave as before.
No new option or help text. The only difference I know of is that a partial commit no longer refreshes vfile.chnged and vfile.mtime of the files it does not commit; status and changes check them anyway.
If you would rather not change the default, the same could be offered as an option.
--- src/checkin.c
+++ src/checkin.c
@@ -2749,6 +2749,29 @@
qsort((void*)sCiInfo.azTag, nTag, sizeof(sCiInfo.azTag[0]), tagCmp);
}
+ /* There are two ways this command may be executed. If there are
+ ** no arguments following the word "commit", then all modified files
+ ** in the checked-out directory are committed. If one or more arguments
+ ** follows "commit", then only those files are committed.
+ **
+ ** After the following function call has returned, the Global.aCommitFile[]
+ ** array is allocated to contain the "id" field from the vfile table
+ ** for each file to be committed. Or, if aCommitFile is NULL, all files
+ ** should be committed.
+ */
+ if( select_commit_files() ){
+ if( !noPrompt ){
+ prompt_user("continue (y/N)? ", &ans);
+ cReply = blob_str(&ans)[0];
+ blob_reset(&ans);
+ }else{
+ cReply = 'N';
+ }
+ if( cReply!='y' && cReply!='Y' ){
+ fossil_exit(1);
+ }
+ }
+
hasChanges = unsaved_changes(useHash ? CKSIG_HASH : 0);
if( hasChanges==0 && onlyIfChanges ){
/* "fossil commit --if-changes" is a no-op if there are no changes. */
@@ -2812,28 +2835,6 @@
}
}
- /* There are two ways this command may be executed. If there are
- ** no arguments following the word "commit", then all modified files
- ** in the checked-out directory are committed. If one or more arguments
- ** follows "commit", then only those files are committed.
- **
- ** After the following function call has returned, the Global.aCommitFile[]
- ** array is allocated to contain the "id" field from the vfile table
- ** for each file to be committed. Or, if aCommitFile is NULL, all files
- ** should be committed.
- */
- if( select_commit_files() ){
- if( !noPrompt ){
- prompt_user("continue (y/N)? ", &ans);
- cReply = blob_str(&ans)[0];
- blob_reset(&ans);
- }else{
- cReply = 'N';
- }
- if( cReply!='y' && cReply!='Y' ){
- fossil_exit(1);
- }
- }
isAMerge = db_exists("SELECT 1 FROM vmerge WHERE id=0 OR id<-2");
if( g.aCommitFile && isAMerge ){
fossil_fatal("cannot do a partial commit of a merge");
--- src/vfile.c
+++ src/vfile.c
@@ -181,7 +181,8 @@
" vfile.mrid, deleted, chnged, uuid, size, mtime,"
" CASE WHEN isexe THEN %d WHEN islink THEN %d ELSE %d END"
" FROM vfile LEFT JOIN blob ON vfile.mrid=blob.rid"
- " WHERE vid=%d ", g.zLocalRoot, PERM_EXE, PERM_LNK, PERM_REG,
+ " WHERE vid=%d AND is_selected(id)",
+ g.zLocalRoot, PERM_EXE, PERM_LNK, PERM_REG,
vid);
while( db_step(&q)==SQLITE_ROW ){
int id, rid, isDeleted;
Delta manifests in this use
With the patch, most of the remaining cost is building the baseline manifest (7.8 MB of F-cards for 58,000 files). With --delta (and forbid-delta-manifests off) Fossil does not build it, and the one-file commit drops from 510 ms to 335 ms (last column above). The tool would commit with --delta and make a baseline now and then. I understand why delta manifests are off by default and have read delta-manifests.md; for commits that name a few files in a large checkout they are still a clear win, so I hope --delta stays available.
Testing
I ran the same steps in two checkouts, one committing with trunk and one with trunk + patch, and compared the files of the new check-in (files_of_checkin('tip')), the output of fossil changes, the messages and the exit codes after each step. They were identical for:
- a named edited file, with another file edited and a third file only touched but not named;
- a renamed file (
fossil mv), naming the new name; - a removed file (
fossil rm); - an added file;
- a named file that did not change ("none of the selected files have changed");
- a name Fossil does not know ("fossil knows nothing about");
- a named directory;
- a commit without names (the unnamed edits of the earlier steps are committed);
- a commit without names when nothing changed ("nothing has changed").
I tested on Windows only.
Have you done more than fossil set -g repo-cksum 0 here?
No, nothing else. The numbers in my post are with repo-cksum off; all other settings are the defaults.
With repo-cksum off, a one-file commit in the 58,000-file checkout takes 1,110 ms. Two steps take most of that time, and both grow with the number of files in the checkout, not with the number of files committed:
- Checking every file of the checkout (
vfile_check_signature(), called fromunsaved_changes()): about 600 ms. The patch removes this step. - Building the full manifest (an F-card for each of the 58,000 files): about 175 ms.
--deltaremoves this step.
With both steps removed, the commit takes 335 ms, about a third of the time. The same holds with repo-cksum on:
repo-cksum | trunk | trunk + patch | trunk + patch + --delta |
|---|---|---|---|
| on (default) | 1,509 ms | 1,043 ms | 683 ms |
| off (my table) | 1,110 ms | 510 ms | 335 ms |
Apart from the patch and --delta, I did not find a setting that skips these two steps.
a one-file commit in the 58,000-file checkout takes 1,110 ms.
58k is almost thrice what Fossil was originally designed to handled. See this old commentary from drh on that topic, the relevant section of which says:
The bigger problem is the huge number of files in the checkout: over 60K. When I was designing Fossil, it looked at the Linux kernel and used it as my upper-bound on the number of files in a project. The Linux kernel has about 20K files. So pkgsrc is about 3x larger than the design load for Fossil.
Experimentation based on that doc is currently underway in another tree to extend the artifact format to be more performant with such repos, but (A) it's an experiment at this point and (B) is in a third-party source tree so requires that fossil implementation. We've currently no idea whether that experiment will really pan out, or whether it will make its way to this tree, but perhaps Dan Shearer or Dan Mestas can enlighten us the current results.
The only difference I know of is that a partial commit no longer refreshes vfile.chnged and vfile.mtime of the files it does not commit; status and changes check them anyway.
The patch sounds overall sensible to me, but that it no longer updates chnged/mtime may (this is unverified) cause problems for other commands which expect to read current status info from there. We'd need to tell them how to update that status themselves, which is easy to to (just a function call) but doesn't really solve the problem because the same performance hit would then just move from checkin to wherever.
Delta manifests in this use
FWIW, i suggest disabling delta manifests. This project and its siblings all use the setting.