@vzn/vx-history 0.0.649 → 0.0.651
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/README.md +3 -1
- package/package.json +2 -2
- package/src/history-server.ts +2 -1
- package/src/index.ts +35 -20
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ import { vxHistory } from '@vzn/vx-history'
|
|
|
19
19
|
export default { plugins: [vxHistory()] }
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
`vxHistory({ window?, assume?, file?, url?, issuer?, repository?, headers?, resources?, memory?, reservations? })` learns from the last `window` invocations (default 20) through core's `
|
|
22
|
+
`vxHistory({ window?, assume?, file?, url?, issuer?, repository?, headers?, resources?, memory?, reservations? })` learns from the last `window` invocations (default 20) through core's `localHistory(cache)`, which reads the cache's index and returns rows; the plugin never touches cache.db. With one worker and two chains of identical shape, the chain history says is slow starts first; with no history yet, the scheduler's structural order stands.
|
|
23
23
|
|
|
24
24
|
`assume` is for the run that has no history: a fresh CI runner. It names durations (task id → ms) for tasks the history has not seen, so a long leaf task — a docs build, a cross-compile — starts first instead of last:
|
|
25
25
|
|
|
@@ -47,6 +47,8 @@ for its tasks' numbers, by task definition, in the refs the run reads,
|
|
|
47
47
|
as a remote cache does: its branch's, the branch it merges into, then
|
|
48
48
|
the default branch's (`baseBranch`, else git's or the CI's). A run
|
|
49
49
|
that reads no ref (a detached HEAD with no default branch) asks nothing.
|
|
50
|
+
A ref over the 512 characters a server takes is said and not read;
|
|
51
|
+
the run reads the others.
|
|
50
52
|
The run starts on its local history and re-ranks the tasks not yet
|
|
51
53
|
started when they land; it never waits for them. At its end it sends the
|
|
52
54
|
timings of the tasks it ran, once: under DTE that is the orchestrator,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-history",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.651",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "History-based scheduling for vx — a `schedule` plugin that orders ready tasks by their expected remaining critical path, learned from the local run history.",
|
|
6
6
|
"keywords": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@vzn/vx": "^0.0.
|
|
40
|
+
"@vzn/vx": "^0.0.651"
|
|
41
41
|
},
|
|
42
42
|
"repository": {
|
|
43
43
|
"type": "git",
|
package/src/history-server.ts
CHANGED
|
@@ -181,7 +181,8 @@ export class HistoryServer {
|
|
|
181
181
|
u.searchParams.set('audience', new URL(this.url).origin)
|
|
182
182
|
const res = await this.askGithub(u, signal)
|
|
183
183
|
if (!res.ok) throw new Error(`GitHub's OIDC endpoint: ${(await refusal(res)).message}`)
|
|
184
|
-
|
|
184
|
+
// quiet: a body that is not JSON holds no token, said below
|
|
185
|
+
const jwt = ((await res.json().catch(() => null)) as { value?: unknown } | null)?.value
|
|
185
186
|
if (typeof jwt !== 'string' || jwt === '') throw new Error("GitHub's OIDC endpoint: no token")
|
|
186
187
|
return { jwt, exp: expiryOf(jwt) }
|
|
187
188
|
})()
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
import {
|
|
15
15
|
currentBranch,
|
|
16
16
|
definePlugin,
|
|
17
|
-
|
|
17
|
+
type LocalHistory,
|
|
18
|
+
localHistory,
|
|
18
19
|
machineMemoryBytes,
|
|
19
20
|
Cache,
|
|
20
21
|
type HistoryTable,
|
|
@@ -108,6 +109,9 @@ export interface VxHistoryOptions {
|
|
|
108
109
|
// hint needs less and reads a 2.5× smaller slice of the run history.
|
|
109
110
|
const DEFAULT_WINDOW = 20
|
|
110
111
|
|
|
112
|
+
/** The longest ref a history server takes in a query (its PROTOCOL.md). */
|
|
113
|
+
const REF_MAX = 512
|
|
114
|
+
|
|
111
115
|
// Each option is read in ONE place that a run's hooks and `vx history`
|
|
112
116
|
// share: two copies of each let either drift unseen (item 805).
|
|
113
117
|
const windowOf = (options: VxHistoryOptions): number =>
|
|
@@ -193,7 +197,7 @@ const readHistory = (
|
|
|
193
197
|
branch: string | null,
|
|
194
198
|
defs: ReadonlyMap<string, string>,
|
|
195
199
|
): Promise<ReadonlyMap<string, TaskResources>> =>
|
|
196
|
-
|
|
200
|
+
localHistory(cache, { recent: windowOf(options), branch, defs }).resourcesFor(ids)
|
|
197
201
|
|
|
198
202
|
/** What each task reserves: learned from the history unless `resources: false`, a declared reservation over either. */
|
|
199
203
|
const reservationsFor = (
|
|
@@ -276,6 +280,11 @@ function namedOf(issuer: unknown, repository: unknown): Named {
|
|
|
276
280
|
return { issuer: issuer.replace(/\/$/, ''), repository: repository.toLowerCase() }
|
|
277
281
|
}
|
|
278
282
|
|
|
283
|
+
/** `url` as a refusal may print it: a `user:pass@` would reach the CI log. */
|
|
284
|
+
function shown(url: string): string {
|
|
285
|
+
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i, '$1***@')
|
|
286
|
+
}
|
|
287
|
+
|
|
279
288
|
/**
|
|
280
289
|
* https, or http to this machine: every request carries the CI token, and
|
|
281
290
|
* http:// to another host sent it in clear.
|
|
@@ -298,12 +307,12 @@ function serverUrlOf(
|
|
|
298
307
|
const from = option === undefined ? 'VX_HISTORY_URL' : 'url'
|
|
299
308
|
if (!secure(url))
|
|
300
309
|
throw new UserError(
|
|
301
|
-
`vxHistory(): ${from} must be https (http only to 127.0.0.1, ::1 or localhost), got ${url}`,
|
|
310
|
+
`vxHistory(): ${from} must be https (http only to 127.0.0.1, ::1 or localhost), got ${shown(url)}`,
|
|
302
311
|
)
|
|
303
312
|
// Each route is the server's /v1/…: one that names /v1 asked /v1/v1/…, and every answer was a 404.
|
|
304
313
|
if (url.endsWith('/v1'))
|
|
305
314
|
throw new UserError(
|
|
306
|
-
`vxHistory(): ${from} is the server's base, without /v1: ${url.slice(0, -3)}`,
|
|
315
|
+
`vxHistory(): ${from} is the server's base, without /v1: ${shown(url.slice(0, -3))}`,
|
|
307
316
|
)
|
|
308
317
|
return url
|
|
309
318
|
}
|
|
@@ -411,14 +420,13 @@ export function vxHistory(options: VxHistoryOptions = {}): VxPlugin {
|
|
|
411
420
|
const ids: string[] = []
|
|
412
421
|
for (const n of nodes.values()) if (n.config.exec?.command !== undefined) ids.push(n.id)
|
|
413
422
|
let table: ReadonlyMap<string, TaskResources>
|
|
414
|
-
let provider:
|
|
423
|
+
let provider: LocalHistory
|
|
415
424
|
try {
|
|
416
|
-
provider =
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
ctx.branch,
|
|
425
|
+
provider = localHistory(ctx.localCache, {
|
|
426
|
+
recent: windowOf(options),
|
|
427
|
+
branch: ctx.branch,
|
|
420
428
|
defs,
|
|
421
|
-
)
|
|
429
|
+
})
|
|
422
430
|
table = await provider.resourcesFor(ids)
|
|
423
431
|
} catch (err) {
|
|
424
432
|
// Failing open: a broken history read costs the ordering and the
|
|
@@ -440,14 +448,22 @@ export function vxHistory(options: VxHistoryOptions = {}): VxPlugin {
|
|
|
440
448
|
// branch it merges into, the default branch (`baseBranch`). `main`
|
|
441
449
|
// was assumed, so a `master` or `baseBranch: 'trunk'` repository
|
|
442
450
|
// read no trunk's numbers; a run with none asks nothing.
|
|
443
|
-
|
|
451
|
+
// A ref the server cannot name was a 400 for the whole query, and
|
|
452
|
+
// the run read no ref's numbers, its base branch's too.
|
|
453
|
+
const refs = [...new Set(ctx.refs)].filter((ref) => {
|
|
454
|
+
if (ref.length <= REF_MAX) return true
|
|
455
|
+
ctx.warn(
|
|
456
|
+
`[vx] vx-history: ${ref} is ${ref.length} characters, over the ${REF_MAX} a history server takes: the run does not read it`,
|
|
457
|
+
)
|
|
458
|
+
return false
|
|
459
|
+
})
|
|
444
460
|
if (remote !== undefined && refs.length > 0) {
|
|
445
461
|
const server = remote
|
|
446
462
|
let late = false
|
|
447
463
|
stopImport = () => (late = true)
|
|
448
464
|
const weights = (async () => {
|
|
449
465
|
const hashes = [...new Set(ids.flatMap((id) => defs.get(id) ?? []))]
|
|
450
|
-
const got = await server.query(
|
|
466
|
+
const got = await server.query(refs, hashes)
|
|
451
467
|
const timings = new Map<string, number>()
|
|
452
468
|
const needs = new Map<string, TaskResources>()
|
|
453
469
|
for (const id of ids) {
|
|
@@ -800,7 +816,7 @@ async function historyCmd(
|
|
|
800
816
|
// A plugin verb owns its help; completions offer `--help` for every verb.
|
|
801
817
|
if (a === '--help' || a === '-h') {
|
|
802
818
|
ui.say('info', 'Usage: vx history [--format pretty|json]', 'out')
|
|
803
|
-
|
|
819
|
+
ui.print('out', ui.gap())
|
|
804
820
|
ui.say(
|
|
805
821
|
'info',
|
|
806
822
|
'What vx-history learned per task (p50, peak RSS, CPU parallelism) and the reservation it packs.',
|
|
@@ -849,13 +865,12 @@ async function historyCmd(
|
|
|
849
865
|
// Each task's rows of its definition as it is now, as `schedule` reads
|
|
850
866
|
// them: read with every definition's, a task whose command changed
|
|
851
867
|
// showed the old command's peak and a reservation no run packs.
|
|
852
|
-
table = await
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
currentBranch(ctx.workspaceRoot),
|
|
868
|
+
table = await localHistory(cache, {
|
|
869
|
+
recent: window,
|
|
870
|
+
branch: currentBranch(ctx.workspaceRoot),
|
|
856
871
|
// As a run reads them: a task whose command changed has no rows yet.
|
|
857
|
-
taskDefinitions(tasks as TaskNode[]),
|
|
858
|
-
).loadFor(ids)
|
|
872
|
+
defs: taskDefinitions(tasks as TaskNode[]),
|
|
873
|
+
}).loadFor(ids)
|
|
859
874
|
} finally {
|
|
860
875
|
cache.close()
|
|
861
876
|
}
|
|
@@ -877,7 +892,7 @@ async function historyCmd(
|
|
|
877
892
|
}
|
|
878
893
|
})
|
|
879
894
|
if (format === 'json') {
|
|
880
|
-
|
|
895
|
+
ui.data('out', `${JSON.stringify({ window, budgets, tasks: rows })}\n`)
|
|
881
896
|
return 0
|
|
882
897
|
}
|
|
883
898
|
// The table's module loads with the verb, off every run's path.
|