Fossil SCM

Progress toward delta compression on the xfer protocol. The compression works well. But the client is not telling the server what files it has so the server does not have anything to delta against.

drh 2007-08-09 03:19 trunk
Commit eea381f416beab14a5356f36f1c9108ba9843b92
--- src/rebuild.c
+++ src/rebuild.c
@@ -46,10 +46,13 @@
4646
usage("REPOSITORY-FILENAME");
4747
}
4848
errCnt = 0;
4949
db_open_repository(g.argv[2]);
5050
db_begin_transaction();
51
+ db_multi_exec(
52
+ "CREATE INDEX IF NOT EXISTS delta_i1 ON delta(srcid);"
53
+ );
5154
for(;;){
5255
zTable = db_text(0,
5356
"SELECT name FROM sqlite_master"
5457
" WHERE type='table'"
5558
" AND name NOT IN ('blob','delta','rcvfrom','user','config')");
5659
--- src/rebuild.c
+++ src/rebuild.c
@@ -46,10 +46,13 @@
46 usage("REPOSITORY-FILENAME");
47 }
48 errCnt = 0;
49 db_open_repository(g.argv[2]);
50 db_begin_transaction();
 
 
 
51 for(;;){
52 zTable = db_text(0,
53 "SELECT name FROM sqlite_master"
54 " WHERE type='table'"
55 " AND name NOT IN ('blob','delta','rcvfrom','user','config')");
56
--- src/rebuild.c
+++ src/rebuild.c
@@ -46,10 +46,13 @@
46 usage("REPOSITORY-FILENAME");
47 }
48 errCnt = 0;
49 db_open_repository(g.argv[2]);
50 db_begin_transaction();
51 db_multi_exec(
52 "CREATE INDEX IF NOT EXISTS delta_i1 ON delta(srcid);"
53 );
54 for(;;){
55 zTable = db_text(0,
56 "SELECT name FROM sqlite_master"
57 " WHERE type='table'"
58 " AND name NOT IN ('blob','delta','rcvfrom','user','config')");
59
--- src/schema.c
+++ src/schema.c
@@ -80,10 +80,11 @@
8080
@ );
8181
@ CREATE TABLE delta(
8282
@ rid INTEGER PRIMARY KEY, -- Record ID
8383
@ srcid INTEGER NOT NULL REFERENCES blob -- Record holding source document
8484
@ );
85
+@ CREATE INDEX delta_i1 ON delta(srcid);
8586
@
8687
@ -- Whenever new blobs are received into the repository, an entry
8788
@ -- in this table records the source of the blob.
8889
@ --
8990
@ CREATE TABLE rcvfrom(
9091
--- src/schema.c
+++ src/schema.c
@@ -80,10 +80,11 @@
80 @ );
81 @ CREATE TABLE delta(
82 @ rid INTEGER PRIMARY KEY, -- Record ID
83 @ srcid INTEGER NOT NULL REFERENCES blob -- Record holding source document
84 @ );
 
85 @
86 @ -- Whenever new blobs are received into the repository, an entry
87 @ -- in this table records the source of the blob.
88 @ --
89 @ CREATE TABLE rcvfrom(
90
--- src/schema.c
+++ src/schema.c
@@ -80,10 +80,11 @@
80 @ );
81 @ CREATE TABLE delta(
82 @ rid INTEGER PRIMARY KEY, -- Record ID
83 @ srcid INTEGER NOT NULL REFERENCES blob -- Record holding source document
84 @ );
85 @ CREATE INDEX delta_i1 ON delta(srcid);
86 @
87 @ -- Whenever new blobs are received into the repository, an entry
88 @ -- in this table records the source of the blob.
89 @ --
90 @ CREATE TABLE rcvfrom(
91
+109 -54
--- src/xfer.c
+++ src/xfer.c
@@ -23,10 +23,83 @@
2323
**
2424
** This file contains code to implement the file transfer protocol.
2525
*/
2626
#include "config.h"
2727
#include "xfer.h"
28
+
29
+/*
30
+** Try to locate a record that is similar to rid and is a likely
31
+** candidate for delta against rid. The similar record must be
32
+** referenced in the onremote table.
33
+**
34
+** Return the integer record ID of the similar record. Or return
35
+** 0 if none is found.
36
+*/
37
+static int similar_record(int rid, int traceFlag){
38
+ int inCnt, outCnt;
39
+ Stmt q;
40
+ int queue[100];
41
+
42
+ db_prepare(&q,
43
+ "SELECT srcid, EXISTS(SELECT 1 FROM onremote WHERE rid=srcid)"
44
+ " FROM delta"
45
+ " WHERE rid=:x"
46
+ " UNION ALL "
47
+ "SELECT rid, EXISTS(SELECT 1 FROM onremote WHERE rid=delta.rid)"
48
+ " FROM delta"
49
+ " WHERE srcid=:x"
50
+ );
51
+ queue[0] = rid;
52
+ inCnt = 1;
53
+ outCnt = 0;
54
+ while( outCnt<inCnt ){
55
+ int xid = queue[outCnt%64];
56
+ outCnt++;
57
+ db_bind_int(&q, ":x", xid);
58
+ if( traceFlag ) printf("xid=%d\n", xid);
59
+ while( db_step(&q)==SQLITE_ROW ){
60
+ int nid = db_column_int(&q, 0);
61
+ int hit = db_column_int(&q, 1);
62
+ if( traceFlag ) printf("nid=%d hit=%d\n", nid, hit);
63
+ if( hit ){
64
+ db_finalize(&q);
65
+ return nid;
66
+ }
67
+ if( inCnt<sizeof(queue)/sizeof(queue[0]) ){
68
+ int i;
69
+ for(i=0; i<inCnt && queue[i]!=nid; i++){}
70
+ if( i>=inCnt ){
71
+ queue[inCnt++] = nid;
72
+ }
73
+ }
74
+ }
75
+ db_reset(&q);
76
+ }
77
+ db_finalize(&q);
78
+ return 0;
79
+}
80
+
81
+/*
82
+** COMMAND: test-similar-record
83
+*/
84
+void test_similar_record(void){
85
+ int i;
86
+ if( g.argc<4 ){
87
+ usage("SRC ONREMOTE...");
88
+ }
89
+ db_must_be_within_tree();
90
+ db_multi_exec(
91
+ "CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);"
92
+ );
93
+ for(i=3; i<g.argc; i++){
94
+ int rid = name_to_rid(g.argv[i]);
95
+ printf("%s -> %d\n", g.argv[i], rid);
96
+ db_multi_exec("INSERT INTO onremote VALUES(%d)", rid);
97
+ }
98
+ printf("similar: %d\n", similar_record(name_to_rid(g.argv[2]), 1));
99
+}
100
+
28101
29102
/*
30103
** The aToken[0..nToken-1] blob array is a parse of a "file" line
31104
** message. This routine finishes parsing that message and does
32105
** a record insert of the file.
@@ -87,43 +160,50 @@
87160
** to pOut. Otherwise, append the text to the CGI output.
88161
*/
89162
static int send_file(int rid, Blob *pOut){
90163
Blob content, uuid;
91164
int size;
92
-
93
-#if 0
94
-SELECT srcid FROM delta
95
- WHERE rid=%d
96
- AND EXISTS(SELECT 1 FROM onremote WHERE rid=srcid)
97
-UNION ALL
98
-SELECT id FROM delta
99
- WHERE srcid=%d
100
- AND EXISTS(SELECT 1 FROM onremote WHERE rid=delta.rid)
101
-LIMIT 1
102
-#endif
103
-
104
- /* TODO:
105
- ** Check for related files in the onremote TEMP table. If related
106
- ** files are found, then send a delta rather than the whole file.
107
- */
165
+ int srcid;
166
+
108167
109168
blob_zero(&uuid);
110169
db_blob(&uuid, "SELECT uuid FROM blob WHERE rid=%d AND size>=0", rid);
111170
if( blob_size(&uuid)==0 ){
112171
return 0;
113172
}
114173
content_get(rid, &content);
115
- size = blob_size(&content);
116
- if( pOut ){
117
- blob_appendf(pOut, "file %b %d\n", &uuid, size);
118
- blob_append(pOut, blob_buffer(&content), size);
174
+
175
+ srcid = similar_record(rid, 0);
176
+ if( srcid ){
177
+ Blob src, delta;
178
+ Blob srcuuid;
179
+ content_get(srcid, &src);
180
+ blob_delta_create(&src, &content, &delta);
181
+ blob_reset(&src);
182
+ blob_reset(&content);
183
+ blob_zero(&srcuuid);
184
+ db_blob(&srcuuid, "SELECT uuid FROM blob WHERE rid=%d", srcid);
185
+ size = blob_size(&delta);
186
+ if( pOut ){
187
+ blob_appendf(pOut, "file %b %b %d\n", &uuid, &srcuuid, size);
188
+ blob_append(pOut, blob_buffer(&delta), size);
189
+ }else{
190
+ cgi_printf("file %b %b %d\n", &uuid, &srcuuid, size);
191
+ cgi_append_content(blob_buffer(&delta), size);
192
+ }
119193
}else{
120
- cgi_printf("file %b %d\n", &uuid, size);
121
- cgi_append_content(blob_buffer(&content), size);
194
+ size = blob_size(&content);
195
+ if( pOut ){
196
+ blob_appendf(pOut, "file %b %d\n", &uuid, size);
197
+ blob_append(pOut, blob_buffer(&content), size);
198
+ }else{
199
+ cgi_printf("file %b %d\n", &uuid, size);
200
+ cgi_append_content(blob_buffer(&content), size);
201
+ }
202
+ blob_reset(&content);
203
+ blob_reset(&uuid);
122204
}
123
- blob_reset(&content);
124
- blob_reset(&uuid);
125205
db_multi_exec("INSERT OR IGNORE INTO onremote VALUES(%d)", rid);
126206
return size;
127207
}
128208
129209
@@ -134,33 +214,11 @@
134214
int iRidSent = 0;
135215
int sent = 0;
136216
int nSent = 0;
137217
int maxSize = db_get_int("http-msg-size", 500000);
138218
Stmt q;
139
-#if 0
140
- db_multi_exec(
141
- "CREATE TEMP TABLE priority(rid INTEGER PRIMARY KEY);"
142
- "INSERT INTO priority"
143
- " SELECT srcid FROM delta"
144
- " WHERE EXISTS(SELECT 1 FROM onremote WHERE onremote.rid=delta.rid)"
145
- " AND EXISTS(SELECT 1 FROM pending WHERE delta.srcid=pending.rid);"
146
- "INSERT OR IGNORE INTO priority"
147
- " SELECT rid FROM delta"
148
- " WHERE EXISTS(SELECT 1 FROM onremote WHERE onremote.rid=delta.srcid)"
149
- " AND EXISTS(SELECT 1 FROM pending WHERE delta.rid=pending.rid);"
150
- );
151
- while( sent<maxSize && (rid = db_int(0, "SELECT rid FROM priority"))!=0 ){
152
- sent += send_file(rid, pOut);
153
- db_multi_exec(
154
- "INSERT OR IGNORE INTO priority"
155
- " SELECT srcid FROM delta WHERE rid=%d"
156
- " UNION ALL"
157
- " SELECT rid FROM delta WHERE srcid=%d",
158
- rid, rid
159
- );
160
- }
161
-#endif
219
+
162220
db_prepare(&q, "SELECT rid FROM pending ORDER BY rid");
163221
while( db_step(&q)==SQLITE_ROW ){
164222
int rid = db_column_int(&q, 0);
165223
if( sent<maxSize ){
166224
sent += send_file(rid, pOut);
@@ -186,13 +244,10 @@
186244
*/
187245
if( nSent>0 ){
188246
db_multi_exec("DELETE FROM pending WHERE rid <= %d", iRidSent);
189247
}
190248
191
-#if 0
192
- db_multi_exec("DROP TABLE priority");
193
-#endif
194249
return nSent;
195250
}
196251
197252
198253
/*
@@ -592,12 +647,12 @@
592647
}
593648
db_finalize(&q);
594649
}
595650
596651
/* Exchange messages with the server */
597
- printf("Send: %d files, %d requests, %d other messages\n",
598
- nFile, nReq, nMsg);
652
+ printf("Send: %3d files, %3d requests, %3d other msgs, %8d bytes\n",
653
+ nFile, nReq, nMsg, blob_size(&send));
599654
nFileSend = nFile;
600655
nFile = nReq = nMsg = 0;
601656
http_exchange(&send, &recv);
602657
blob_reset(&send);
603658
@@ -704,13 +759,13 @@
704759
if( blob_size(&errmsg) ){
705760
fossil_fatal("%b", &errmsg);
706761
}
707762
blobarray_reset(aToken, nToken);
708763
}
764
+ printf("Received: %3d files, %3d requests, %3d other msgs, %8d bytes\n",
765
+ nFile, nReq, nMsg, blob_size(&recv));
709766
blob_reset(&recv);
710
- printf("Received: %d files, %d requests, %d other messages\n",
711
- nFile, nReq, nMsg);
712767
if( nFileSend + nFile==0 ){
713768
nNoFileCycle++;
714769
if( nNoFileCycle>1 ){
715770
go = 0;
716771
}
717772
--- src/xfer.c
+++ src/xfer.c
@@ -23,10 +23,83 @@
23 **
24 ** This file contains code to implement the file transfer protocol.
25 */
26 #include "config.h"
27 #include "xfer.h"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
29 /*
30 ** The aToken[0..nToken-1] blob array is a parse of a "file" line
31 ** message. This routine finishes parsing that message and does
32 ** a record insert of the file.
@@ -87,43 +160,50 @@
87 ** to pOut. Otherwise, append the text to the CGI output.
88 */
89 static int send_file(int rid, Blob *pOut){
90 Blob content, uuid;
91 int size;
92
93 #if 0
94 SELECT srcid FROM delta
95 WHERE rid=%d
96 AND EXISTS(SELECT 1 FROM onremote WHERE rid=srcid)
97 UNION ALL
98 SELECT id FROM delta
99 WHERE srcid=%d
100 AND EXISTS(SELECT 1 FROM onremote WHERE rid=delta.rid)
101 LIMIT 1
102 #endif
103
104 /* TODO:
105 ** Check for related files in the onremote TEMP table. If related
106 ** files are found, then send a delta rather than the whole file.
107 */
108
109 blob_zero(&uuid);
110 db_blob(&uuid, "SELECT uuid FROM blob WHERE rid=%d AND size>=0", rid);
111 if( blob_size(&uuid)==0 ){
112 return 0;
113 }
114 content_get(rid, &content);
115 size = blob_size(&content);
116 if( pOut ){
117 blob_appendf(pOut, "file %b %d\n", &uuid, size);
118 blob_append(pOut, blob_buffer(&content), size);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119 }else{
120 cgi_printf("file %b %d\n", &uuid, size);
121 cgi_append_content(blob_buffer(&content), size);
 
 
 
 
 
 
 
 
122 }
123 blob_reset(&content);
124 blob_reset(&uuid);
125 db_multi_exec("INSERT OR IGNORE INTO onremote VALUES(%d)", rid);
126 return size;
127 }
128
129
@@ -134,33 +214,11 @@
134 int iRidSent = 0;
135 int sent = 0;
136 int nSent = 0;
137 int maxSize = db_get_int("http-msg-size", 500000);
138 Stmt q;
139 #if 0
140 db_multi_exec(
141 "CREATE TEMP TABLE priority(rid INTEGER PRIMARY KEY);"
142 "INSERT INTO priority"
143 " SELECT srcid FROM delta"
144 " WHERE EXISTS(SELECT 1 FROM onremote WHERE onremote.rid=delta.rid)"
145 " AND EXISTS(SELECT 1 FROM pending WHERE delta.srcid=pending.rid);"
146 "INSERT OR IGNORE INTO priority"
147 " SELECT rid FROM delta"
148 " WHERE EXISTS(SELECT 1 FROM onremote WHERE onremote.rid=delta.srcid)"
149 " AND EXISTS(SELECT 1 FROM pending WHERE delta.rid=pending.rid);"
150 );
151 while( sent<maxSize && (rid = db_int(0, "SELECT rid FROM priority"))!=0 ){
152 sent += send_file(rid, pOut);
153 db_multi_exec(
154 "INSERT OR IGNORE INTO priority"
155 " SELECT srcid FROM delta WHERE rid=%d"
156 " UNION ALL"
157 " SELECT rid FROM delta WHERE srcid=%d",
158 rid, rid
159 );
160 }
161 #endif
162 db_prepare(&q, "SELECT rid FROM pending ORDER BY rid");
163 while( db_step(&q)==SQLITE_ROW ){
164 int rid = db_column_int(&q, 0);
165 if( sent<maxSize ){
166 sent += send_file(rid, pOut);
@@ -186,13 +244,10 @@
186 */
187 if( nSent>0 ){
188 db_multi_exec("DELETE FROM pending WHERE rid <= %d", iRidSent);
189 }
190
191 #if 0
192 db_multi_exec("DROP TABLE priority");
193 #endif
194 return nSent;
195 }
196
197
198 /*
@@ -592,12 +647,12 @@
592 }
593 db_finalize(&q);
594 }
595
596 /* Exchange messages with the server */
597 printf("Send: %d files, %d requests, %d other messages\n",
598 nFile, nReq, nMsg);
599 nFileSend = nFile;
600 nFile = nReq = nMsg = 0;
601 http_exchange(&send, &recv);
602 blob_reset(&send);
603
@@ -704,13 +759,13 @@
704 if( blob_size(&errmsg) ){
705 fossil_fatal("%b", &errmsg);
706 }
707 blobarray_reset(aToken, nToken);
708 }
 
 
709 blob_reset(&recv);
710 printf("Received: %d files, %d requests, %d other messages\n",
711 nFile, nReq, nMsg);
712 if( nFileSend + nFile==0 ){
713 nNoFileCycle++;
714 if( nNoFileCycle>1 ){
715 go = 0;
716 }
717
--- src/xfer.c
+++ src/xfer.c
@@ -23,10 +23,83 @@
23 **
24 ** This file contains code to implement the file transfer protocol.
25 */
26 #include "config.h"
27 #include "xfer.h"
28
29 /*
30 ** Try to locate a record that is similar to rid and is a likely
31 ** candidate for delta against rid. The similar record must be
32 ** referenced in the onremote table.
33 **
34 ** Return the integer record ID of the similar record. Or return
35 ** 0 if none is found.
36 */
37 static int similar_record(int rid, int traceFlag){
38 int inCnt, outCnt;
39 Stmt q;
40 int queue[100];
41
42 db_prepare(&q,
43 "SELECT srcid, EXISTS(SELECT 1 FROM onremote WHERE rid=srcid)"
44 " FROM delta"
45 " WHERE rid=:x"
46 " UNION ALL "
47 "SELECT rid, EXISTS(SELECT 1 FROM onremote WHERE rid=delta.rid)"
48 " FROM delta"
49 " WHERE srcid=:x"
50 );
51 queue[0] = rid;
52 inCnt = 1;
53 outCnt = 0;
54 while( outCnt<inCnt ){
55 int xid = queue[outCnt%64];
56 outCnt++;
57 db_bind_int(&q, ":x", xid);
58 if( traceFlag ) printf("xid=%d\n", xid);
59 while( db_step(&q)==SQLITE_ROW ){
60 int nid = db_column_int(&q, 0);
61 int hit = db_column_int(&q, 1);
62 if( traceFlag ) printf("nid=%d hit=%d\n", nid, hit);
63 if( hit ){
64 db_finalize(&q);
65 return nid;
66 }
67 if( inCnt<sizeof(queue)/sizeof(queue[0]) ){
68 int i;
69 for(i=0; i<inCnt && queue[i]!=nid; i++){}
70 if( i>=inCnt ){
71 queue[inCnt++] = nid;
72 }
73 }
74 }
75 db_reset(&q);
76 }
77 db_finalize(&q);
78 return 0;
79 }
80
81 /*
82 ** COMMAND: test-similar-record
83 */
84 void test_similar_record(void){
85 int i;
86 if( g.argc<4 ){
87 usage("SRC ONREMOTE...");
88 }
89 db_must_be_within_tree();
90 db_multi_exec(
91 "CREATE TEMP TABLE onremote(rid INTEGER PRIMARY KEY);"
92 );
93 for(i=3; i<g.argc; i++){
94 int rid = name_to_rid(g.argv[i]);
95 printf("%s -> %d\n", g.argv[i], rid);
96 db_multi_exec("INSERT INTO onremote VALUES(%d)", rid);
97 }
98 printf("similar: %d\n", similar_record(name_to_rid(g.argv[2]), 1));
99 }
100
101
102 /*
103 ** The aToken[0..nToken-1] blob array is a parse of a "file" line
104 ** message. This routine finishes parsing that message and does
105 ** a record insert of the file.
@@ -87,43 +160,50 @@
160 ** to pOut. Otherwise, append the text to the CGI output.
161 */
162 static int send_file(int rid, Blob *pOut){
163 Blob content, uuid;
164 int size;
165 int srcid;
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
168 blob_zero(&uuid);
169 db_blob(&uuid, "SELECT uuid FROM blob WHERE rid=%d AND size>=0", rid);
170 if( blob_size(&uuid)==0 ){
171 return 0;
172 }
173 content_get(rid, &content);
174
175 srcid = similar_record(rid, 0);
176 if( srcid ){
177 Blob src, delta;
178 Blob srcuuid;
179 content_get(srcid, &src);
180 blob_delta_create(&src, &content, &delta);
181 blob_reset(&src);
182 blob_reset(&content);
183 blob_zero(&srcuuid);
184 db_blob(&srcuuid, "SELECT uuid FROM blob WHERE rid=%d", srcid);
185 size = blob_size(&delta);
186 if( pOut ){
187 blob_appendf(pOut, "file %b %b %d\n", &uuid, &srcuuid, size);
188 blob_append(pOut, blob_buffer(&delta), size);
189 }else{
190 cgi_printf("file %b %b %d\n", &uuid, &srcuuid, size);
191 cgi_append_content(blob_buffer(&delta), size);
192 }
193 }else{
194 size = blob_size(&content);
195 if( pOut ){
196 blob_appendf(pOut, "file %b %d\n", &uuid, size);
197 blob_append(pOut, blob_buffer(&content), size);
198 }else{
199 cgi_printf("file %b %d\n", &uuid, size);
200 cgi_append_content(blob_buffer(&content), size);
201 }
202 blob_reset(&content);
203 blob_reset(&uuid);
204 }
 
 
205 db_multi_exec("INSERT OR IGNORE INTO onremote VALUES(%d)", rid);
206 return size;
207 }
208
209
@@ -134,33 +214,11 @@
214 int iRidSent = 0;
215 int sent = 0;
216 int nSent = 0;
217 int maxSize = db_get_int("http-msg-size", 500000);
218 Stmt q;
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220 db_prepare(&q, "SELECT rid FROM pending ORDER BY rid");
221 while( db_step(&q)==SQLITE_ROW ){
222 int rid = db_column_int(&q, 0);
223 if( sent<maxSize ){
224 sent += send_file(rid, pOut);
@@ -186,13 +244,10 @@
244 */
245 if( nSent>0 ){
246 db_multi_exec("DELETE FROM pending WHERE rid <= %d", iRidSent);
247 }
248
 
 
 
249 return nSent;
250 }
251
252
253 /*
@@ -592,12 +647,12 @@
647 }
648 db_finalize(&q);
649 }
650
651 /* Exchange messages with the server */
652 printf("Send: %3d files, %3d requests, %3d other msgs, %8d bytes\n",
653 nFile, nReq, nMsg, blob_size(&send));
654 nFileSend = nFile;
655 nFile = nReq = nMsg = 0;
656 http_exchange(&send, &recv);
657 blob_reset(&send);
658
@@ -704,13 +759,13 @@
759 if( blob_size(&errmsg) ){
760 fossil_fatal("%b", &errmsg);
761 }
762 blobarray_reset(aToken, nToken);
763 }
764 printf("Received: %3d files, %3d requests, %3d other msgs, %8d bytes\n",
765 nFile, nReq, nMsg, blob_size(&recv));
766 blob_reset(&recv);
 
 
767 if( nFileSend + nFile==0 ){
768 nNoFileCycle++;
769 if( nNoFileCycle>1 ){
770 go = 0;
771 }
772

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button