Skip to content

Commit c983a2b

Browse files
code-asherkylecarbs
authored andcommitted
Watcher and initial load performance improvements (#357)
* Set low CPU priority on watcher Fixes #247. * Batch stat and readdir calls * Fix fs.exists callbackify seems to always adds an error as the first argument. Opted to just use the promise for this one. * Batch lstat * Add maximum time for flushing batches
1 parent a5ce135 commit c983a2b

File tree

4 files changed

+136
-8
lines changed

4 files changed

+136
-8
lines changed

packages/protocol/src/browser/modules/fs.ts

+44-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
11
import * as fs from "fs";
22
import { callbackify } from "util";
3-
import { ClientProxy } from "../../common/proxy";
3+
import { ClientProxy, Batch } from "../../common/proxy";
44
import { IEncodingOptions, IEncodingOptionsCallback } from "../../common/util";
55
import { FsModuleProxy, Stats as IStats, WatcherProxy, WriteStreamProxy } from "../../node/modules/fs";
66
import { Writable } from "./stream";
77

88
// tslint:disable no-any
99

10+
class StatBatch extends Batch<IStats, { path: fs.PathLike }> {
11+
public constructor(private readonly proxy: FsModuleProxy) {
12+
super();
13+
}
14+
15+
protected remoteCall(batch: { path: fs.PathLike }[]): Promise<(IStats | Error)[]> {
16+
return this.proxy.statBatch(batch);
17+
}
18+
}
19+
20+
class LstatBatch extends Batch<IStats, { path: fs.PathLike }> {
21+
public constructor(private readonly proxy: FsModuleProxy) {
22+
super();
23+
}
24+
25+
protected remoteCall(batch: { path: fs.PathLike }[]): Promise<(IStats | Error)[]> {
26+
return this.proxy.lstatBatch(batch);
27+
}
28+
}
29+
30+
class ReaddirBatch extends Batch<Buffer[] | fs.Dirent[] | string[], { path: fs.PathLike, options: IEncodingOptions }> {
31+
public constructor(private readonly proxy: FsModuleProxy) {
32+
super();
33+
}
34+
35+
protected remoteCall(queue: { path: fs.PathLike, options: IEncodingOptions }[]): Promise<(Buffer[] | fs.Dirent[] | string[] | Error)[]> {
36+
return this.proxy.readdirBatch(queue);
37+
}
38+
}
39+
1040
class Watcher extends ClientProxy<WatcherProxy> implements fs.FSWatcher {
1141
public close(): void {
1242
this.proxy.close();
@@ -28,7 +58,15 @@ class WriteStream extends Writable<WriteStreamProxy> implements fs.WriteStream {
2858
}
2959

3060
export class FsModule {
31-
public constructor(private readonly proxy: FsModuleProxy) {}
61+
private readonly statBatch: StatBatch;
62+
private readonly lstatBatch: LstatBatch;
63+
private readonly readdirBatch: ReaddirBatch;
64+
65+
public constructor(private readonly proxy: FsModuleProxy) {
66+
this.statBatch = new StatBatch(this.proxy);
67+
this.lstatBatch = new LstatBatch(this.proxy);
68+
this.readdirBatch = new ReaddirBatch(this.proxy);
69+
}
3270

3371
public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
3472
if (typeof mode === "function") {
@@ -72,9 +110,7 @@ export class FsModule {
72110
}
73111

74112
public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => {
75-
callbackify(this.proxy.exists)(path, (exists) => {
76-
callback!(exists as any);
77-
});
113+
this.proxy.exists(path).then((exists) => callback(exists)).catch(() => callback(false));
78114
}
79115

80116
public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
@@ -124,7 +160,7 @@ export class FsModule {
124160
}
125161

126162
public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
127-
callbackify(this.proxy.lstat)(path, (error, stats) => {
163+
callbackify(this.lstatBatch.add)({ path }, (error, stats) => {
128164
callback(error, stats && new Stats(stats));
129165
});
130166
}
@@ -175,7 +211,7 @@ export class FsModule {
175211
callback = options;
176212
options = undefined;
177213
}
178-
callbackify(this.proxy.readdir)(path, options, callback!);
214+
callbackify(this.readdirBatch.add)({ path, options }, callback!);
179215
}
180216

181217
public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => {
@@ -203,7 +239,7 @@ export class FsModule {
203239
}
204240

205241
public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
206-
callbackify(this.proxy.stat)(path, (error, stats) => {
242+
callbackify(this.statBatch.add)({ path }, (error, stats) => {
207243
callback(error, stats && new Stats(stats));
208244
});
209245
}

packages/protocol/src/common/proxy.ts

+71
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,74 @@ export enum Module {
8181
NodePty = "node-pty",
8282
Trash = "trash",
8383
}
84+
85+
interface BatchItem<T, A> {
86+
args: A;
87+
resolve: (t: T) => void;
88+
reject: (e: Error) => void;
89+
}
90+
91+
/**
92+
* Batch remote calls.
93+
*/
94+
export abstract class Batch<T, A> {
95+
private idleTimeout: number | NodeJS.Timer | undefined;
96+
private maxTimeout: number | NodeJS.Timer | undefined;
97+
private batch = <BatchItem<T, A>[]>[];
98+
99+
public constructor(
100+
/**
101+
* Flush after reaching this amount of time.
102+
*/
103+
private readonly maxTime = 1000,
104+
/**
105+
* Flush after reaching this count.
106+
*/
107+
private readonly maxCount = 100,
108+
/**
109+
* Flush after not receiving more requests for this amount of time.
110+
*/
111+
private readonly idleTime = 100,
112+
) {}
113+
114+
public add = (args: A): Promise<T> => {
115+
return new Promise((resolve, reject) => {
116+
this.batch.push({
117+
args,
118+
resolve,
119+
reject,
120+
});
121+
if (this.batch.length >= this.maxCount) {
122+
this.flush();
123+
} else {
124+
clearTimeout(this.idleTimeout as any);
125+
this.idleTimeout = setTimeout(this.flush, this.idleTime);
126+
if (typeof this.maxTimeout === "undefined") {
127+
this.maxTimeout = setTimeout(this.flush, this.maxTime);
128+
}
129+
}
130+
});
131+
}
132+
133+
protected abstract remoteCall(batch: A[]): Promise<(T | Error)[]>;
134+
135+
private flush = (): void => {
136+
clearTimeout(this.idleTimeout as any);
137+
clearTimeout(this.maxTimeout as any);
138+
this.maxTimeout = undefined;
139+
140+
const batch = this.batch;
141+
this.batch = [];
142+
143+
this.remoteCall(batch.map((q) => q.args)).then((results) => {
144+
batch.forEach((item, i) => {
145+
const result = results[i];
146+
if (result && result instanceof Error) {
147+
item.reject(result);
148+
} else {
149+
item.resolve(result);
150+
}
151+
});
152+
}).catch((error) => batch.forEach((item) => item.reject(error)));
153+
}
154+
}

packages/protocol/src/node/modules/fs.ts

+12
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export class FsModuleProxy {
156156
return this.makeStatsSerializable(await promisify(fs.lstat)(path));
157157
}
158158

159+
public async lstatBatch(args: { path: fs.PathLike }[]): Promise<(Stats | Error)[]> {
160+
return Promise.all(args.map((a) => this.lstat(a.path).catch((e) => e)));
161+
}
162+
159163
public mkdir(path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null): Promise<void> {
160164
return promisify(fs.mkdir)(path, mode);
161165
}
@@ -182,6 +186,10 @@ export class FsModuleProxy {
182186
return promisify(fs.readdir)(path, options);
183187
}
184188

189+
public readdirBatch(args: { path: fs.PathLike, options: IEncodingOptions }[]): Promise<(Buffer[] | fs.Dirent[] | string[] | Error)[]> {
190+
return Promise.all(args.map((a) => this.readdir(a.path, a.options).catch((e) => e)));
191+
}
192+
185193
public readlink(path: fs.PathLike, options: IEncodingOptions): Promise<string | Buffer> {
186194
return promisify(fs.readlink)(path, options);
187195
}
@@ -202,6 +210,10 @@ export class FsModuleProxy {
202210
return this.makeStatsSerializable(await promisify(fs.stat)(path));
203211
}
204212

213+
public async statBatch(args: { path: fs.PathLike }[]): Promise<(Stats | Error)[]> {
214+
return Promise.all(args.map((a) => this.stat(a.path).catch((e) => e)));
215+
}
216+
205217
public symlink(target: fs.PathLike, path: fs.PathLike, type?: fs.symlink.Type | null): Promise<void> {
206218
return promisify(fs.symlink)(target, path, type);
207219
}

packages/server/src/vscode/bootstrapFork.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as cp from "child_process";
22
import * as fs from "fs";
3+
import * as os from "os";
34
import * as path from "path";
45
import * as vm from "vm";
6+
import { logger } from "@coder/logger";
57
import { buildDir, isCli } from "../constants";
68

79
let ipcMsgBuffer: Buffer[] | undefined = [];
@@ -151,6 +153,13 @@ export const forkModule = (modulePath: string, args?: string[], options?: cp.For
151153
} else {
152154
proc = cp.spawn(process.execPath, ["--require", "ts-node/register", "--require", "tsconfig-paths/register", process.argv[1], ...forkArgs], forkOptions);
153155
}
156+
if (args && args[0] === "--type=watcherService" && os.platform() === "linux") {
157+
cp.exec(`renice -n 19 -p ${proc.pid}`, (error) => {
158+
if (error) {
159+
logger.warn(error.message);
160+
}
161+
});
162+
}
154163

155164
return proc;
156165
};

0 commit comments

Comments
 (0)