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.
Files changed (48) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/environment.js +89 -1
  5. package/kernel/git.js +9 -19
  6. package/kernel/index.js +142 -43
  7. package/kernel/launch_requirements.js +1115 -0
  8. package/kernel/ready.js +231 -0
  9. package/kernel/script.js +16 -0
  10. package/kernel/shells.js +9 -1
  11. package/kernel/workspace_status.js +111 -45
  12. package/package.json +1 -1
  13. package/server/autolaunch.js +725 -0
  14. package/server/index.js +244 -411
  15. package/server/views/app.ejs +256 -152
  16. package/server/views/autolaunch.ejs +363 -75
  17. package/server/views/index.ejs +550 -26
  18. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  19. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  20. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  21. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  22. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  23. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  24. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  25. package/server/views/partials/home_action_modal.ejs +4 -1
  26. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  27. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  28. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  29. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  30. package/server/views/terminal.ejs +196 -2
  31. package/test/home-autolaunch-live-ui.test.js +455 -0
  32. package/test/launch-requirements-browser.test.js +579 -0
  33. package/test/launch-requirements-contract-coverage.test.js +627 -0
  34. package/test/launch-requirements-real-browser.js +625 -0
  35. package/test/launch-requirements-status-client.test.js +132 -0
  36. package/test/launch-requirements.test.js +1806 -0
  37. package/test/launch-settings-ui.test.js +370 -0
  38. package/test/ready-state.test.js +49 -0
  39. package/test/server-autolaunch.test.js +1052 -0
  40. package/test/startup-git-index-benchmark.js +409 -0
  41. package/test/startup-git-index-browser.js +320 -0
  42. package/test/startup-git-index-performance.test.js +380 -0
  43. package/test/startup-git-index-refactor.test.js +450 -0
  44. package/test/startup-git-index-route.test.js +588 -0
  45. package/test/universal-launcher.smoke.spec.js +10 -9
  46. package/test/workspace-gitignore-benchmark.js +815 -0
  47. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  48. 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
+ })