backlens 0.2.2 → 0.2.3
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/ai/fixes.js +28 -6
- package/package.json +1 -1
- package/server.js +6 -0
- package/ui.html +9 -8
package/ai/fixes.js
CHANGED
|
@@ -20,6 +20,9 @@ import { promiseAll } from './templates.js';
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
// Replaying a write sends it again, so it changes the dev database twice: reads only unless asked.
|
|
23
|
+
// Replays after a fix: warm-up runs first, then measured runs (reads only).
|
|
24
|
+
const WARMUPS = 3;
|
|
25
|
+
const SAMPLES = 5;
|
|
23
26
|
const DEFAULTS = { provider: null, ollamaModel: null, dailyBudgetUsd: 1, replay: true, replayWrites: false };
|
|
24
27
|
const EDIT_REPAIRS = 2;
|
|
25
28
|
const TYPE_REPAIRS = 2;
|
|
@@ -483,6 +486,12 @@ export function createAI(deps) {
|
|
|
483
486
|
return { ok: r.code === 0, errors: r.out };
|
|
484
487
|
}
|
|
485
488
|
|
|
489
|
+
const median = (list) => {
|
|
490
|
+
const xs = [...list].sort((a, b) => a - b);
|
|
491
|
+
const mid = Math.floor(xs.length / 2);
|
|
492
|
+
return Math.round((xs.length % 2 ? xs[mid] : (xs[mid - 1] + xs[mid]) / 2) * 100) / 100;
|
|
493
|
+
};
|
|
494
|
+
|
|
486
495
|
/** What a request cost: the numbers shown before and after a fix. */
|
|
487
496
|
function stats(trace) {
|
|
488
497
|
const count = (kind) => trace.spans.filter((x) => x.kind === kind).length;
|
|
@@ -556,20 +565,33 @@ export function createAI(deps) {
|
|
|
556
565
|
} else {
|
|
557
566
|
// The app has just restarted, so its first request pays for cold caches and new connections.
|
|
558
567
|
// A read is sent once to warm up, and the second one is measured.
|
|
559
|
-
|
|
560
|
-
|
|
568
|
+
// A just-restarted app is slow for its first requests (cold code, new database
|
|
569
|
+
// connections). A read is sent a few times to warm up, then measured several times and the
|
|
570
|
+
// median kept. A write runs once: replaying it changes data again.
|
|
571
|
+
const samples = [];
|
|
572
|
+
if (!isWrite) for (let i = 0; i < WARMUPS; i++) await replay(job, payload, deps.appPort());
|
|
573
|
+
for (let i = 0; i < (isWrite ? 1 : SAMPLES); i++) {
|
|
574
|
+
const t = await replay(job, payload, deps.appPort());
|
|
575
|
+
if (!t) break;
|
|
576
|
+
samples.push(t);
|
|
577
|
+
}
|
|
578
|
+
const trace = samples.at(-1);
|
|
561
579
|
if (!trace) {
|
|
562
580
|
r.status = 'fail';
|
|
563
581
|
r.detail = 'the replayed request did not finish within 20 s';
|
|
564
582
|
return finish(job, false, 'The replayed request did not come back.');
|
|
565
583
|
}
|
|
566
584
|
replayed = true;
|
|
567
|
-
const still = checkTrace(
|
|
585
|
+
const still = samples.some((t) => checkTrace(t).some((i) => sameIssue(i, job.issue)));
|
|
568
586
|
const sameClass = original ? Math.floor(trace.status / 100) === Math.floor(original.status / 100) : trace.status < 400;
|
|
569
|
-
|
|
587
|
+
// Before: the same route's recorded requests from before the change, not just one of them.
|
|
588
|
+
const earlier = (deps.routeTraces ? deps.routeTraces(job.issue.route) : []).filter((t) => t.status === (original || trace).status);
|
|
589
|
+
const beforeList = earlier.length ? earlier.slice(-SAMPLES) : original ? [original] : [];
|
|
590
|
+
const after = { ...stats(trace), ms: median(samples.map((t) => t.duration)) };
|
|
591
|
+
const before = beforeList.length ? { ...stats(original || beforeList.at(-1)), ms: median(beforeList.map((t) => t.duration)) } : null;
|
|
570
592
|
r.status = !still && sameClass ? 'done' : 'fail';
|
|
571
|
-
r.detail = `before: ${before}\nafter: ${
|
|
572
|
-
job.compare = { before
|
|
593
|
+
r.detail = `before: ${before ? `${before.status}, median ${before.ms} ms of ${beforeList.length} recorded, ${before.queries} queries` : 'no recorded request'}\nafter: ${after.status}, median ${after.ms} ms of ${samples.length} replays, ${after.queries} queries${still ? ', issue still there' : ', issue gone'}`;
|
|
594
|
+
job.compare = { before, after, beforeRuns: beforeList.length, afterRuns: samples.length, issueGone: !still, warmed: !isWrite };
|
|
573
595
|
job.afterTrace = trace;
|
|
574
596
|
if (trace.status === 401 && original && original.status !== 401) r.detail += '\nThe recorded login may have expired: sign in again and use the page once more.';
|
|
575
597
|
if (r.status === 'fail') return finish(job, false, still ? 'The issue is still there after the change.' : `The request now answers ${trace.status}.`);
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -422,6 +422,12 @@ export function startServer({ port, root = process.cwd(), token, manualArgv = nu
|
|
|
422
422
|
return issues.find((i) => i.key === key) || (testRun.report ? testRun.report.issues.find((i) => i.key === key) : null);
|
|
423
423
|
},
|
|
424
424
|
getTrace: (id) => traces.find((t) => t.id === id) || testRun.traces.find((t) => t.id === id),
|
|
425
|
+
// Recorded requests on a route since the last fix kept there (the code they ran is the
|
|
426
|
+
// code being changed now), oldest first.
|
|
427
|
+
routeTraces: (route) => {
|
|
428
|
+
const since = Math.max(0, ...[...fixedAt].filter(([key]) => key.split('|')[1] === route).map(([, at]) => at));
|
|
429
|
+
return traces.filter((t) => t.at > since && routeOf(t.method, t.url, t.template) === route);
|
|
430
|
+
},
|
|
425
431
|
markFixed: (key, after) => {
|
|
426
432
|
fixedAt.set(key, Date.now());
|
|
427
433
|
if (after) {
|
package/ui.html
CHANGED
|
@@ -157,7 +157,8 @@
|
|
|
157
157
|
a.btn { text-decoration: none; display: inline-block; }
|
|
158
158
|
.compare-card { border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; margin: 4px 0 14px; }
|
|
159
159
|
.compare-card h4 { margin: 0 0 8px; font-size: 13px; }
|
|
160
|
-
.compare-card table { width: auto; border-collapse: collapse; font: 13px var(--mono); }
|
|
160
|
+
.compare-card table { width: auto; max-width: 100%; border-collapse: collapse; font: 13px var(--mono); margin-bottom: 10px; }
|
|
161
|
+
.compare-card td, .compare-card th { white-space: nowrap; }
|
|
161
162
|
.compare-card th { text-align: left; font-weight: 400; color: var(--sub); padding: 3px 18px 3px 0; }
|
|
162
163
|
.compare-card td { padding: 3px 10px 3px 0; }
|
|
163
164
|
.compare-card .arrow { color: var(--sub); }
|
|
@@ -769,20 +770,20 @@ function compareCard(j) {
|
|
|
769
770
|
const c = j.compare;
|
|
770
771
|
if (!c || !c.before || !['verified', 'failed', 'unverified', 'kept', 'reverted'].includes(j.stage)) return '';
|
|
771
772
|
const pct = (a, b) => (a ? Math.round(((b - a) / a) * 100) : 0);
|
|
772
|
-
const row = (label, a, b, fmt = (x) => x,
|
|
773
|
+
const row = (label, a, b, fmt = (x) => x, noise = 0) => {
|
|
773
774
|
const d = pct(a, b);
|
|
774
|
-
|
|
775
|
-
|
|
775
|
+
// Small time differences are noise, not a result: shown plain, not green or red.
|
|
776
|
+
const tone = a === b || Math.abs(b - a) <= noise ? 'same' : b < a ? 'better' : 'worse';
|
|
777
|
+
return `<tr><th>${label}</th><td>${fmt(a)}</td><td class="arrow">→</td><td class="${tone}">${fmt(b)}</td><td class="${tone} delta">${tone === 'same' ? '' : `${d > 0 ? '+' : ''}${d}%`}</td></tr>`;
|
|
776
778
|
};
|
|
777
|
-
const kind = esc((j.issueTitle || '').split(':')[0]);
|
|
778
779
|
return `<section class="compare-card"><h4>Same request, before and after the change</h4><table>
|
|
779
|
-
${row('time', c.before.ms, c.after.ms, (x) => ms(x))}
|
|
780
|
+
${row('time (median)', c.before.ms, c.after.ms, (x) => ms(x), Math.max(3, c.before.ms * 0.15))}
|
|
780
781
|
${row('database queries', c.before.queries, c.after.queries)}
|
|
781
782
|
${c.before.redis || c.after.redis ? row('redis calls', c.before.redis, c.after.redis) : ''}
|
|
782
783
|
${c.before.http || c.after.http ? row('outgoing http', c.before.http, c.after.http) : ''}
|
|
783
784
|
<tr><th>status</th><td>${c.before.status}</td><td class="arrow">→</td><td class="${c.before.status === c.after.status ? 'same' : 'worse'}">${c.after.status}</td><td></td></tr>
|
|
784
|
-
<tr><th>issue</th><td
|
|
785
|
-
</table><p class="note">
|
|
785
|
+
<tr><th>issue</th><td>found</td><td class="arrow">→</td><td class="${c.issueGone ? 'better' : 'worse'}">${c.issueGone ? 'gone' : 'still there'}</td><td></td></tr>
|
|
786
|
+
</table><p class="note">Before: ${c.beforeRuns} recorded request${c.beforeRuns === 1 ? '' : 's'}. After: ${c.afterRuns} replay${c.afterRuns === 1 ? '' : 's'}${c.warmed ? ', after warming the restarted app up' : ''}. Query counts are exact; a time change within a few ms is noise.</p></section>`;
|
|
786
787
|
}
|
|
787
788
|
|
|
788
789
|
// ---- creating a suggested index from the drawer ----------------------------------------
|