Skip to content

Commit b9ea5de

Browse files
perf: speed up hot processing (#5803)
1 parent 2c0ff5f commit b9ea5de

8 files changed

Lines changed: 178 additions & 15 deletions

File tree

lib/box/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,10 @@ class Box extends EventEmitter {
141141

142142
// Handle deleted files
143143
return this._readDir(base)
144-
.then(files => cacheFiles.filter(path => !files.includes(path)))
144+
.then(files => {
145+
const fileSet = new Set(files);
146+
return cacheFiles.filter(path => !fileSet.has(path));
147+
})
145148
.map(path => this._processFile(File.TYPE_DELETE, path));
146149
}).catch(err => {
147150
if (err && err.code !== 'ENOENT') throw err;

lib/plugins/console/generate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,9 @@ class Generator {
172172
const task = (fn, path) => () => fn.call(this, path);
173173
const doTask = fn => fn();
174174
const routeList = route.list();
175+
const routeSet = new Set(routeList);
175176
const publicFiles = Cache.filter(item => item._id.startsWith('public/')).map(item => item._id.substring(7));
176-
const tasks = publicFiles.filter(path => !routeList.includes(path))
177+
const tasks = publicFiles.filter(path => !routeSet.has(path))
177178
// Clean files
178179
.map(path => task(this.deleteFile, path))
179180
// Generate files

lib/plugins/processor/asset.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import type { _File } from '../../box';
88
import type Hexo from '../../hexo';
99
import type { Stats } from 'fs';
1010
import { PageSchema } from '../../types';
11+
import SourceIdIndex from './source_id_index';
1112

1213
export = (ctx: Hexo) => {
14+
const pageSourceIndex = new SourceIdIndex(ctx.model('Page'));
15+
1316
return {
1417
pattern: new Pattern(path => {
1518
if (isExcludedFile(path, ctx.config)) return;
@@ -21,18 +24,18 @@ export = (ctx: Hexo) => {
2124

2225
process: function assetProcessor(file: _File) {
2326
if (file.params.renderable) {
24-
return processPage(ctx, file);
27+
return processPage(ctx, file, pageSourceIndex);
2528
}
2629

2730
return processAsset(ctx, file);
2831
}
2932
};
3033
};
3134

32-
function processPage(ctx: Hexo, file: _File) {
35+
function processPage(ctx: Hexo, file: _File, sourceIndex: SourceIdIndex<PageSchema>) {
3336
const Page = ctx.model('Page');
3437
const { path } = file;
35-
const doc = Page.findOne({source: path});
38+
const doc = sourceIndex.find(path);
3639
const { config } = ctx;
3740
const { timezone } = config;
3841
const updated_option = config.updated_option;
@@ -43,7 +46,10 @@ function processPage(ctx: Hexo, file: _File) {
4346

4447
if (file.type === 'delete') {
4548
if (doc) {
46-
return doc.remove();
49+
return doc.remove().then(result => {
50+
sourceIndex.delete(path);
51+
return result;
52+
});
4753
}
4854

4955
return;
@@ -106,6 +112,9 @@ function processPage(ctx: Hexo, file: _File) {
106112
}
107113

108114
return Page.insert(data);
115+
}).then((doc: PageSchema) => {
116+
sourceIndex.set(doc);
117+
return doc;
109118
});
110119
}
111120

lib/plugins/processor/post.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type Hexo from '../../hexo';
1010
import type { Stats } from 'fs';
1111
import { PostAssetSchema, PostSchema } from '../../types';
1212
import type Document from 'warehouse/dist/document';
13+
import SourceIdIndex from './source_id_index';
1314

1415
const postDir = '_posts/';
1516
const draftDir = '_drafts/';
@@ -26,6 +27,8 @@ const preservedKeys = {
2627
};
2728

2829
export = (ctx: Hexo) => {
30+
const postSourceIndex = new SourceIdIndex(ctx.model('Post'));
31+
2932
return {
3033
pattern: new Pattern(path => {
3134
if (isTmpFile(path)) return;
@@ -59,18 +62,18 @@ export = (ctx: Hexo) => {
5962

6063
process: function postProcessor(file: _File) {
6164
if (file.params.renderable) {
62-
return processPost(ctx, file);
65+
return processPost(ctx, file, postSourceIndex);
6366
} else if (ctx.config.post_asset_folder) {
6467
return processAsset(ctx, file);
6568
}
6669
}
6770
};
6871
};
6972

70-
function processPost(ctx: Hexo, file: _File) {
73+
function processPost(ctx: Hexo, file: _File, sourceIndex: SourceIdIndex<PostSchema>) {
7174
const Post = ctx.model('Post');
7275
const { path } = file.params;
73-
const doc = Post.findOne({source: file.path});
76+
const doc = sourceIndex.find(file.path);
7477
const { config } = ctx;
7578
const { timezone, updated_option, use_slug_as_post_title } = config;
7679

@@ -82,7 +85,10 @@ function processPost(ctx: Hexo, file: _File) {
8285

8386
if (file.type === 'delete') {
8487
if (doc) {
85-
return doc.remove();
88+
return doc.remove().then(result => {
89+
sourceIndex.delete(file.path);
90+
return result;
91+
});
8692
}
8793

8894
return;
@@ -184,11 +190,15 @@ function processPost(ctx: Hexo, file: _File) {
184190
}
185191

186192
return Post.insert(data);
187-
}).then((doc: PostSchema) => Promise.all([
188-
doc.setCategories(categories),
189-
doc.setTags(tags),
190-
scanAssetDir(ctx, doc)
191-
]).then(() => markFuturePostDirty(ctx, file, doc)));
193+
}).then((doc: PostSchema) => {
194+
sourceIndex.set(doc);
195+
196+
return Promise.all([
197+
doc.setCategories(categories),
198+
doc.setTags(tags),
199+
scanAssetDir(ctx, doc)
200+
]).then(() => markFuturePostDirty(ctx, file, doc));
201+
});
192202
}
193203

194204
function parseFilename(config: string, path: string) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type Document from 'warehouse/dist/document';
2+
import type Model from 'warehouse/dist/model';
3+
4+
interface SourceSchema {
5+
_id?: string;
6+
source: string;
7+
}
8+
9+
class SourceIdIndex<T extends SourceSchema> {
10+
private readonly model: Model<T>;
11+
private sourceIds?: Map<string, string>;
12+
13+
constructor(model: Model<T>) {
14+
this.model = model;
15+
}
16+
17+
find(source: string): Document<T> | undefined {
18+
const sourceIds = this.load();
19+
const id = sourceIds.get(source);
20+
21+
if (id) {
22+
const doc = this.model.findById(id);
23+
if (doc?.source === source) return doc;
24+
sourceIds.delete(source);
25+
}
26+
27+
const doc = this.model.findOne({source});
28+
if (doc) this.set(doc);
29+
return doc;
30+
}
31+
32+
set(doc: T | Document<T>): void {
33+
if (doc._id) this.load().set(doc.source, doc._id);
34+
}
35+
36+
delete(source: string): void {
37+
this.load().delete(source);
38+
}
39+
40+
private load(): Map<string, string> {
41+
if (!this.sourceIds) {
42+
this.sourceIds = new Map();
43+
this.model.forEach(doc => {
44+
if (doc._id) this.sourceIds.set(doc.source, doc._id);
45+
});
46+
}
47+
48+
return this.sourceIds;
49+
}
50+
}
51+
52+
export default SourceIdIndex;

test/scripts/box/box.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,35 @@ describe('Box', () => {
229229
await rmdir(box.base);
230230
});
231231

232+
it('process() - delete only stale cache entries', async () => {
233+
const box = newBox('test');
234+
const existingName = 'existing.txt';
235+
const deletedName = 'deleted.txt';
236+
const processor = spy();
237+
box.addProcessor(processor);
238+
239+
await BluebirdPromise.all([
240+
writeFile(join(box.base, existingName), 'existing'),
241+
box.Cache.insert({
242+
_id: `test/${existingName}`,
243+
modified: 0,
244+
hash: hash('existing').toString('hex')
245+
}),
246+
box.Cache.insert({
247+
_id: `test/${deletedName}`
248+
})
249+
]);
250+
await box.process();
251+
252+
const deletedFiles = processor.args
253+
.map(([file]) => file)
254+
.filter(file => file.type === 'delete');
255+
deletedFiles.should.have.lengthOf(1);
256+
deletedFiles[0].path.should.eql(deletedName);
257+
258+
await rmdir(box.base);
259+
});
260+
232261
it('process() - params', async () => {
233262
const box = newBox('test');
234263
const path = join(box.base, 'posts', '123456');

test/scripts/console/generate.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,25 @@ describe('generate', () => {
135135
]);
136136
});
137137

138+
it('delete only stale generated files', async () => {
139+
let routes = ['existing.txt', 'deleted.txt'];
140+
hexo.extend.generator.register('stale_routes', () => routes.map(path => ({
141+
path,
142+
data: path
143+
})));
144+
145+
await generate();
146+
routes = ['existing.txt'];
147+
await generate();
148+
149+
const result = await BluebirdPromise.all([
150+
exists(join(hexo.public_dir, 'existing.txt')),
151+
exists(join(hexo.public_dir, 'deleted.txt'))
152+
]);
153+
result[0].should.be.true;
154+
result[1].should.be.false;
155+
});
156+
138157
it('force regenerate', async () => {
139158
const src = join(hexo.source_dir, 'test.txt');
140159
const dest = join(hexo.public_dir, 'test.txt');
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { join } from 'path';
2+
import { assert as sinonAssert, spy } from 'sinon';
3+
import Hexo from '../../../lib/hexo';
4+
import SourceIdIndex from '../../../lib/plugins/processor/source_id_index';
5+
import chai from 'chai';
6+
7+
chai.should();
8+
9+
describe('SourceIdIndex', () => {
10+
it('uses the document id for an indexed source', async () => {
11+
const hexo = new Hexo(join(__dirname, 'source_id_index_test'));
12+
const Post = hexo.model('Post');
13+
const doc = await Post.insert({source: '_posts/foo.md', slug: 'foo'});
14+
const index = new SourceIdIndex(Post);
15+
const findOne = spy(Post, 'findOne');
16+
17+
const result = index.find(doc.source);
18+
19+
result._id.should.eql(doc._id);
20+
sinonAssert.notCalled(findOne);
21+
findOne.restore();
22+
});
23+
24+
it('recovers if a cached document was replaced externally', async () => {
25+
const hexo = new Hexo(join(__dirname, 'source_id_index_test'));
26+
const Post = hexo.model('Post');
27+
const first = await Post.insert({source: '_posts/foo.md', slug: 'foo'});
28+
const index = new SourceIdIndex(Post);
29+
index.find(first.source)._id.should.eql(first._id);
30+
31+
await first.remove();
32+
const second = await Post.insert({source: '_posts/foo.md', slug: 'foo'});
33+
const findOne = spy(Post, 'findOne');
34+
35+
index.find(second.source)._id.should.eql(second._id);
36+
index.find(second.source)._id.should.eql(second._id);
37+
sinonAssert.calledOnce(findOne);
38+
findOne.restore();
39+
});
40+
});

0 commit comments

Comments
 (0)