pinokiod 7.3.15 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kernel/api/index.js +111 -15
- package/kernel/api/script/index.js +10 -0
- package/kernel/autolaunch.js +111 -0
- package/kernel/environment.js +89 -1
- package/kernel/git.js +9 -19
- package/kernel/index.js +142 -43
- package/kernel/launch_requirements.js +1115 -0
- package/kernel/ready.js +231 -0
- package/kernel/script.js +16 -0
- package/kernel/shells.js +9 -1
- package/kernel/workspace_status.js +111 -45
- package/package.json +1 -1
- package/server/autolaunch.js +725 -0
- package/server/index.js +244 -411
- package/server/views/app.ejs +256 -152
- package/server/views/autolaunch.ejs +363 -75
- package/server/views/index.ejs +550 -26
- package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
- package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
- package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
- package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
- package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
- package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
- package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
- package/server/views/partials/home_action_modal.ejs +4 -1
- package/server/views/partials/launch_requirements_status_client.ejs +271 -0
- package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
- package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
- package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
- package/server/views/terminal.ejs +196 -2
- package/test/home-autolaunch-live-ui.test.js +455 -0
- package/test/launch-requirements-browser.test.js +579 -0
- package/test/launch-requirements-contract-coverage.test.js +627 -0
- package/test/launch-requirements-real-browser.js +625 -0
- package/test/launch-requirements-status-client.test.js +132 -0
- package/test/launch-requirements.test.js +1806 -0
- package/test/launch-settings-ui.test.js +370 -0
- package/test/ready-state.test.js +49 -0
- package/test/server-autolaunch.test.js +1052 -0
- package/test/startup-git-index-benchmark.js +409 -0
- package/test/startup-git-index-browser.js +320 -0
- package/test/startup-git-index-performance.test.js +380 -0
- package/test/startup-git-index-refactor.test.js +450 -0
- package/test/startup-git-index-route.test.js +588 -0
- package/test/universal-launcher.smoke.spec.js +10 -9
- package/test/workspace-gitignore-benchmark.js +815 -0
- package/test/workspace-gitignore-path-scoped.test.js +256 -0
- package/spec/INSTRUCTION_SYNC.md +0 -432
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const os = require('node:os')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
const { spawn, execFileSync } = require('node:child_process')
|
|
7
|
+
const { performance } = require('node:perf_hooks')
|
|
8
|
+
|
|
9
|
+
const Git = require('../kernel/git')
|
|
10
|
+
const WorkspaceStatusManager = require('../kernel/workspace_status')
|
|
11
|
+
|
|
12
|
+
const FS_METHODS = ['readdir', 'readFile', 'stat', 'lstat', 'access']
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
return [
|
|
16
|
+
'Usage: node test/startup-git-index-benchmark.js [options]',
|
|
17
|
+
'',
|
|
18
|
+
'Options:',
|
|
19
|
+
' --home <path> Pinokio home. Defaults to $PINOKIO_HOME or ~/pinokio.',
|
|
20
|
+
' --api-root <path> API root. Defaults to <home>/api.',
|
|
21
|
+
' --iterations <n> Iterations per case. Defaults to 3.',
|
|
22
|
+
' --cases <list> Comma-separated cases. Defaults to git.index,workspace.ensureWatcher(api).',
|
|
23
|
+
' --out <path> Output JSON path. Defaults to ~/.codex/benchmarks/pinokiod-startup/focused-<timestamp>.json.',
|
|
24
|
+
' --disk <device> Also sample system-wide iostat for the device, for example disk0.',
|
|
25
|
+
' --help Show this help.',
|
|
26
|
+
'',
|
|
27
|
+
'This is a focused component benchmark. After the refactor, direct git.index() may still be slow;',
|
|
28
|
+
'the startup-path proof must separately show startup no longer calls it.',
|
|
29
|
+
].join('\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const args = {
|
|
34
|
+
home: process.env.PINOKIO_HOME || path.join(os.homedir(), 'pinokio'),
|
|
35
|
+
apiRoot: null,
|
|
36
|
+
iterations: 3,
|
|
37
|
+
cases: ['git.index', 'workspace.ensureWatcher(api)'],
|
|
38
|
+
out: null,
|
|
39
|
+
disk: null,
|
|
40
|
+
help: false,
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const arg = argv[i]
|
|
44
|
+
const next = () => {
|
|
45
|
+
i += 1
|
|
46
|
+
if (i >= argv.length) {
|
|
47
|
+
throw new Error(`${arg} requires a value`)
|
|
48
|
+
}
|
|
49
|
+
return argv[i]
|
|
50
|
+
}
|
|
51
|
+
if (arg === '--help' || arg === '-h') {
|
|
52
|
+
args.help = true
|
|
53
|
+
} else if (arg === '--home') {
|
|
54
|
+
args.home = path.resolve(next())
|
|
55
|
+
} else if (arg === '--api-root') {
|
|
56
|
+
args.apiRoot = path.resolve(next())
|
|
57
|
+
} else if (arg === '--iterations') {
|
|
58
|
+
args.iterations = Number.parseInt(next(), 10)
|
|
59
|
+
} else if (arg === '--cases') {
|
|
60
|
+
args.cases = next().split(',').map((item) => item.trim()).filter(Boolean)
|
|
61
|
+
} else if (arg === '--out') {
|
|
62
|
+
args.out = path.resolve(next())
|
|
63
|
+
} else if (arg === '--disk') {
|
|
64
|
+
args.disk = next()
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error(`Unknown argument: ${arg}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!args.apiRoot) {
|
|
70
|
+
args.apiRoot = path.join(args.home, 'api')
|
|
71
|
+
}
|
|
72
|
+
if (!Number.isInteger(args.iterations) || args.iterations < 1) {
|
|
73
|
+
throw new Error('--iterations must be a positive integer')
|
|
74
|
+
}
|
|
75
|
+
if (!args.out) {
|
|
76
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
77
|
+
args.out = path.join(os.homedir(), '.codex', 'benchmarks', 'pinokiod-startup', `focused-${stamp}.json`)
|
|
78
|
+
}
|
|
79
|
+
return args
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeEmptyFsStats() {
|
|
83
|
+
const byMethod = {}
|
|
84
|
+
for (const method of FS_METHODS) {
|
|
85
|
+
byMethod[method] = { count: 0, errors: 0, timeMs: 0 }
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
byMethod,
|
|
89
|
+
readdirEntries: 0,
|
|
90
|
+
readFileBytes: 0,
|
|
91
|
+
gitignoreReadFiles: 0,
|
|
92
|
+
gitignoreReadBytes: 0,
|
|
93
|
+
errorCodes: {},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function installFsInstrumentation() {
|
|
98
|
+
const originals = {}
|
|
99
|
+
let current = null
|
|
100
|
+
|
|
101
|
+
for (const method of FS_METHODS) {
|
|
102
|
+
originals[method] = fs.promises[method]
|
|
103
|
+
fs.promises[method] = async function instrumentedFsMethod(...args) {
|
|
104
|
+
const stats = current
|
|
105
|
+
const started = performance.now()
|
|
106
|
+
try {
|
|
107
|
+
const result = await originals[method].apply(this, args)
|
|
108
|
+
if (stats) {
|
|
109
|
+
const bucket = stats.byMethod[method]
|
|
110
|
+
bucket.count += 1
|
|
111
|
+
bucket.timeMs += performance.now() - started
|
|
112
|
+
if (method === 'readdir' && Array.isArray(result)) {
|
|
113
|
+
stats.readdirEntries += result.length
|
|
114
|
+
} else if (method === 'readFile') {
|
|
115
|
+
const byteLength = Buffer.isBuffer(result) ? result.length : Buffer.byteLength(String(result))
|
|
116
|
+
stats.readFileBytes += byteLength
|
|
117
|
+
if (path.basename(String(args[0])) === '.gitignore') {
|
|
118
|
+
stats.gitignoreReadFiles += 1
|
|
119
|
+
stats.gitignoreReadBytes += byteLength
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return result
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (stats) {
|
|
126
|
+
const bucket = stats.byMethod[method]
|
|
127
|
+
bucket.count += 1
|
|
128
|
+
bucket.errors += 1
|
|
129
|
+
bucket.timeMs += performance.now() - started
|
|
130
|
+
const code = error && error.code ? error.code : 'UNKNOWN'
|
|
131
|
+
stats.errorCodes[code] = (stats.errorCodes[code] || 0) + 1
|
|
132
|
+
}
|
|
133
|
+
throw error
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
runWithStats: async (fn) => {
|
|
140
|
+
const stats = makeEmptyFsStats()
|
|
141
|
+
current = stats
|
|
142
|
+
try {
|
|
143
|
+
const payload = await fn()
|
|
144
|
+
return { stats, payload }
|
|
145
|
+
} finally {
|
|
146
|
+
current = null
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
restore: () => {
|
|
150
|
+
for (const method of FS_METHODS) {
|
|
151
|
+
fs.promises[method] = originals[method]
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createKernel(home, apiRoot) {
|
|
158
|
+
const kernel = {
|
|
159
|
+
homedir: home,
|
|
160
|
+
envs: { ...process.env },
|
|
161
|
+
path: (...parts) => {
|
|
162
|
+
if (parts[0] === 'api') {
|
|
163
|
+
return path.join(apiRoot, ...parts.slice(1))
|
|
164
|
+
}
|
|
165
|
+
return path.join(home, ...parts)
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
kernel.git = new Git(kernel)
|
|
169
|
+
return kernel
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resourceSnapshot() {
|
|
173
|
+
return {
|
|
174
|
+
usage: process.resourceUsage(),
|
|
175
|
+
memory: process.memoryUsage(),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function resourceDelta(before, after) {
|
|
180
|
+
return {
|
|
181
|
+
userCpuMs: (after.usage.userCPUTime - before.usage.userCPUTime) / 1000,
|
|
182
|
+
systemCpuMs: (after.usage.systemCPUTime - before.usage.systemCPUTime) / 1000,
|
|
183
|
+
fsRead: after.usage.fsRead - before.usage.fsRead,
|
|
184
|
+
fsWrite: after.usage.fsWrite - before.usage.fsWrite,
|
|
185
|
+
majorPageFault: after.usage.majorPageFault - before.usage.majorPageFault,
|
|
186
|
+
minorPageFault: after.usage.minorPageFault - before.usage.minorPageFault,
|
|
187
|
+
voluntaryContextSwitches: after.usage.voluntaryContextSwitches - before.usage.voluntaryContextSwitches,
|
|
188
|
+
involuntaryContextSwitches: after.usage.involuntaryContextSwitches - before.usage.involuntaryContextSwitches,
|
|
189
|
+
maxRSSKb: after.usage.maxRSS,
|
|
190
|
+
rssBeforeBytes: before.memory.rss,
|
|
191
|
+
rssAfterBytes: after.memory.rss,
|
|
192
|
+
heapUsedBeforeBytes: before.memory.heapUsed,
|
|
193
|
+
heapUsedAfterBytes: after.memory.heapUsed,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sleep(ms) {
|
|
198
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function withIostat(device, fn) {
|
|
202
|
+
if (!device) {
|
|
203
|
+
return { result: await fn(), iostat: null, rawIostat: null }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const child = spawn('iostat', ['-Id', device, '1'], {
|
|
207
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
208
|
+
})
|
|
209
|
+
let stdout = ''
|
|
210
|
+
let stderr = ''
|
|
211
|
+
child.stdout.on('data', (chunk) => {
|
|
212
|
+
stdout += chunk.toString()
|
|
213
|
+
})
|
|
214
|
+
child.stderr.on('data', (chunk) => {
|
|
215
|
+
stderr += chunk.toString()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
await sleep(1200)
|
|
219
|
+
let result
|
|
220
|
+
try {
|
|
221
|
+
result = await fn()
|
|
222
|
+
} finally {
|
|
223
|
+
child.kill('SIGINT')
|
|
224
|
+
await new Promise((resolve) => child.once('close', resolve))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (stderr.trim()) {
|
|
228
|
+
stdout += `\n[stderr]\n${stderr}`
|
|
229
|
+
}
|
|
230
|
+
return { result, iostat: parseIostat(stdout), rawIostat: stdout }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseIostat(raw) {
|
|
234
|
+
const rows = []
|
|
235
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
236
|
+
const trimmed = line.trim()
|
|
237
|
+
if (!trimmed || trimmed.includes('KB/t') || trimmed.includes('disk')) {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
const match = trimmed.match(/^([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)$/)
|
|
241
|
+
if (!match) {
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
rows.push({
|
|
245
|
+
kbPerTransfer: Number(match[1]),
|
|
246
|
+
transfers: Number(match[2]),
|
|
247
|
+
mb: Number(match[3]),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
const intervalSamples = rows.slice(1)
|
|
251
|
+
const totalMb = intervalSamples.reduce((sum, sample) => sum + sample.mb, 0)
|
|
252
|
+
const maxMbPerSec = intervalSamples.reduce((max, sample) => Math.max(max, sample.mb), 0)
|
|
253
|
+
const avgMbPerSec = intervalSamples.length ? totalMb / intervalSamples.length : 0
|
|
254
|
+
return {
|
|
255
|
+
rawSampleCount: rows.length,
|
|
256
|
+
intervalSampleCount: intervalSamples.length,
|
|
257
|
+
totalMb,
|
|
258
|
+
totalTransfers: intervalSamples.reduce((sum, sample) => sum + sample.transfers, 0),
|
|
259
|
+
maxMbPerSec,
|
|
260
|
+
avgMbPerSec,
|
|
261
|
+
intervalSamples,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function cleanupWatchers(manager) {
|
|
266
|
+
for (const subscription of manager.watchers.values()) {
|
|
267
|
+
if (subscription && typeof subscription.unsubscribe === 'function') {
|
|
268
|
+
await subscription.unsubscribe()
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function runCase(name, context) {
|
|
274
|
+
if (name === 'git.index') {
|
|
275
|
+
const kernel = createKernel(context.home, context.apiRoot)
|
|
276
|
+
await kernel.git.index(kernel)
|
|
277
|
+
return {
|
|
278
|
+
dirs: kernel.git.dirs.size,
|
|
279
|
+
mappings: Object.keys(kernel.git.mapping).length,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (name === 'workspace.ensureWatcher(api)') {
|
|
284
|
+
const manager = new WorkspaceStatusManager({ enableWatchers: true })
|
|
285
|
+
try {
|
|
286
|
+
await manager.ensureWatcher('api', context.apiRoot)
|
|
287
|
+
return {
|
|
288
|
+
watchers: manager.watchers.size,
|
|
289
|
+
gitIgnoreEngines: manager.gitIgnoreEngines.size,
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
await cleanupWatchers(manager)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
throw new Error(`Unknown benchmark case: ${name}`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function measureOne(name, context, instrumentation) {
|
|
300
|
+
const started = performance.now()
|
|
301
|
+
const resourceBefore = resourceSnapshot()
|
|
302
|
+
try {
|
|
303
|
+
const { result, iostat, rawIostat } = await withIostat(context.disk, async () => (
|
|
304
|
+
instrumentation.runWithStats(async () => runCase(name, context))
|
|
305
|
+
))
|
|
306
|
+
const resourceAfter = resourceSnapshot()
|
|
307
|
+
return {
|
|
308
|
+
name,
|
|
309
|
+
ok: true,
|
|
310
|
+
error: null,
|
|
311
|
+
metrics: {
|
|
312
|
+
wallMs: performance.now() - started,
|
|
313
|
+
...resourceDelta(resourceBefore, resourceAfter),
|
|
314
|
+
},
|
|
315
|
+
fs: result.stats,
|
|
316
|
+
iostat,
|
|
317
|
+
rawIostat,
|
|
318
|
+
payload: result.payload,
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const resourceAfter = resourceSnapshot()
|
|
322
|
+
return {
|
|
323
|
+
name,
|
|
324
|
+
ok: false,
|
|
325
|
+
error: error && error.stack ? error.stack : String(error),
|
|
326
|
+
metrics: {
|
|
327
|
+
wallMs: performance.now() - started,
|
|
328
|
+
...resourceDelta(resourceBefore, resourceAfter),
|
|
329
|
+
},
|
|
330
|
+
fs: makeEmptyFsStats(),
|
|
331
|
+
iostat: null,
|
|
332
|
+
rawIostat: null,
|
|
333
|
+
payload: null,
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function gitCommit(repoRoot) {
|
|
339
|
+
try {
|
|
340
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
341
|
+
cwd: repoRoot,
|
|
342
|
+
encoding: 'utf8',
|
|
343
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
344
|
+
}).trim()
|
|
345
|
+
} catch (_) {
|
|
346
|
+
return null
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function main() {
|
|
351
|
+
const args = parseArgs(process.argv.slice(2))
|
|
352
|
+
if (args.help) {
|
|
353
|
+
console.log(usage())
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const repoRoot = path.resolve(__dirname, '..')
|
|
358
|
+
const context = {
|
|
359
|
+
home: args.home,
|
|
360
|
+
apiRoot: args.apiRoot,
|
|
361
|
+
disk: args.disk,
|
|
362
|
+
}
|
|
363
|
+
const instrumentation = installFsInstrumentation()
|
|
364
|
+
const results = []
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
for (let iteration = 1; iteration <= args.iterations; iteration++) {
|
|
368
|
+
for (const name of args.cases) {
|
|
369
|
+
const test = await measureOne(name, context, instrumentation)
|
|
370
|
+
results.push({ iteration, test })
|
|
371
|
+
const readdir = test.fs && test.fs.byMethod && test.fs.byMethod.readdir
|
|
372
|
+
? test.fs.byMethod.readdir.count
|
|
373
|
+
: 0
|
|
374
|
+
console.error(`${name} iteration ${iteration}: ${test.ok ? 'ok' : 'failed'}, wallMs=${test.metrics.wallMs.toFixed(1)}, readdir=${readdir}`)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} finally {
|
|
378
|
+
instrumentation.restore()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const report = {
|
|
382
|
+
kind: 'pinokiod-startup-focused-component-benchmark',
|
|
383
|
+
createdAt: new Date().toISOString(),
|
|
384
|
+
repoRoot,
|
|
385
|
+
home: args.home,
|
|
386
|
+
apiRoot: args.apiRoot,
|
|
387
|
+
commit: gitCommit(repoRoot),
|
|
388
|
+
node: process.version,
|
|
389
|
+
platform: process.platform,
|
|
390
|
+
iterations: args.iterations,
|
|
391
|
+
cases: args.cases,
|
|
392
|
+
diskDevice: args.disk,
|
|
393
|
+
notes: [
|
|
394
|
+
'This is a focused component benchmark, not proof that these operations run at startup.',
|
|
395
|
+
'After the startup refactor, direct git.index() may remain expensive; startup-path instrumentation must prove startup no longer calls it.',
|
|
396
|
+
args.disk ? 'iostat samples are system-wide, not process-scoped.' : 'No OS disk sampler was enabled.',
|
|
397
|
+
],
|
|
398
|
+
results,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
await fs.promises.mkdir(path.dirname(args.out), { recursive: true })
|
|
402
|
+
await fs.promises.writeFile(args.out, `${JSON.stringify(report, null, 2)}\n`)
|
|
403
|
+
console.log(args.out)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
main().catch((error) => {
|
|
407
|
+
console.error(error && error.stack ? error.stack : error)
|
|
408
|
+
process.exitCode = 1
|
|
409
|
+
})
|