@lensmcp/core 1.25.39 → 1.25.41
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/index.d.ts +1 -0
- package/index.js +1 -1
- package/lib/performance-window.d.ts +141 -0
- package/lib/performance-window.js +2 -0
- package/package.json +2 -2
package/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export * from './lib/service-hop.js';
|
|
|
19
19
|
export * from './lib/flows-config.js';
|
|
20
20
|
export * from './lib/patterns.js';
|
|
21
21
|
export * from './lib/thresholds.js';
|
|
22
|
+
export * from './lib/performance-window.js';
|
|
22
23
|
export * from './lib/event-loop.js';
|
|
23
24
|
export * from './lib/event-loop-history.js';
|
|
24
25
|
export * from './lib/graph-from-events.js';
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";export*from"./lib/common.js";export*from"./lib/ulid.js";export*from"./lib/event-bus.js";export*from"./lib/event-redactor.js";export*from"./lib/resource-store.js";export*from"./lib/answer-budget.js";export*from"./lib/graph-store.js";export*from"./lib/fingerprint.js";export*from"./lib/reducer-registry.js";export*from"./lib/story.js";export*from"./lib/route-template.js";export*from"./lib/sql-shape.js";export*from"./lib/http-shape.js";export*from"./lib/flow-shape.js";export*from"./lib/flow-segments.js";export*from"./lib/flow-categories.js";export*from"./lib/headless-flow.js";export*from"./lib/service-hop.js";export*from"./lib/flows-config.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";export*from"./lib/event-loop.js";export*from"./lib/event-loop-history.js";export*from"./lib/graph-from-events.js";
|
|
1
|
+
"use strict";export*from"./lib/common.js";export*from"./lib/ulid.js";export*from"./lib/event-bus.js";export*from"./lib/event-redactor.js";export*from"./lib/resource-store.js";export*from"./lib/answer-budget.js";export*from"./lib/graph-store.js";export*from"./lib/fingerprint.js";export*from"./lib/reducer-registry.js";export*from"./lib/story.js";export*from"./lib/route-template.js";export*from"./lib/sql-shape.js";export*from"./lib/http-shape.js";export*from"./lib/flow-shape.js";export*from"./lib/flow-segments.js";export*from"./lib/flow-categories.js";export*from"./lib/headless-flow.js";export*from"./lib/service-hop.js";export*from"./lib/flows-config.js";export*from"./lib/patterns.js";export*from"./lib/thresholds.js";export*from"./lib/performance-window.js";export*from"./lib/event-loop.js";export*from"./lib/event-loop-history.js";export*from"./lib/graph-from-events.js";
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { BaseEvent } from '@lensmcp/protocol-types';
|
|
2
|
+
import { type LensmcpThresholds } from './thresholds.js';
|
|
3
|
+
/**
|
|
4
|
+
* How one checkout performed over one window — the routes it served, the statements it ran, the pages it loaded — and
|
|
5
|
+
* which of them broke the workspace's limits (planning/team/07-goal-archive-and-performance-gate.md).
|
|
6
|
+
*
|
|
7
|
+
* A goal's worktree is where the lens measures a goal's work: its QA run must pass these limits (GA-D1), and the goal's
|
|
8
|
+
* archive keeps the report (GA-D4). Pure: events in, a report out. A caller reads the events of a checkout and a window
|
|
9
|
+
* from the history (`EventStore.scan`) and adds them in any order; the report is the same.
|
|
10
|
+
*
|
|
11
|
+
* The rules are the lens's own, so the gate and the dashboard never disagree about what is slow:
|
|
12
|
+
* - a route, a statement or a page is slow when it ran at or above its limit MORE THAN ONCE (the perf reducer's rule —
|
|
13
|
+
* a one-off spike, the first call after a rebuild, is not a breach; it is still in the numbers);
|
|
14
|
+
* - N+1: one request ran the same statement `nPlusOne` times or more;
|
|
15
|
+
* - DB in a loop: more than `dbInLoop` DB calls inside one loop.
|
|
16
|
+
* A streaming response's time is how long it stayed open, not how slow it was: it is counted, never timed. A page the dev
|
|
17
|
+
* server reloaded (`cause: 'hmr'`) is left out, and a hard load is timed from the app's entry (`appReadyMs`), not from
|
|
18
|
+
* the dev server serving it.
|
|
19
|
+
*/
|
|
20
|
+
/** The history kinds the window reads — what a caller asks `EventStore.scan` for. */
|
|
21
|
+
export declare const PERFORMANCE_EVENT_KINDS: readonly ["server-request", "db-query", "loop", "page-visit"];
|
|
22
|
+
/** The limits a window is judged by: the workspace's lens thresholds, for the kinds measured here. */
|
|
23
|
+
export type PerformanceLimits = Pick<LensmcpThresholds, 'slowRouteMs' | 'slowQueryMs' | 'nPlusOne' | 'dbInLoop' | 'slowPageMs'>;
|
|
24
|
+
/**
|
|
25
|
+
* A breach the person waived (`.lensmcp/team/performance.json` `ignore`): matched by route (`GET /health`, `GET /admin/*`),
|
|
26
|
+
* statement (its `sqlId`, or words of its text) or page path — `*` stands for anything — and why. It is still reported,
|
|
27
|
+
* as waived.
|
|
28
|
+
*/
|
|
29
|
+
export interface PerformanceWaiver {
|
|
30
|
+
route?: string;
|
|
31
|
+
sql?: string;
|
|
32
|
+
page?: string;
|
|
33
|
+
why: string;
|
|
34
|
+
}
|
|
35
|
+
export interface PerformanceTiming {
|
|
36
|
+
/** Runs counted (a statement's flood-suppressed repeats included). */
|
|
37
|
+
count: number;
|
|
38
|
+
/** Runs timed — the ones the percentiles are of. */
|
|
39
|
+
timed: number;
|
|
40
|
+
p50Ms: number;
|
|
41
|
+
p95Ms: number;
|
|
42
|
+
maxMs: number;
|
|
43
|
+
/** Runs at or above the limit. */
|
|
44
|
+
slow: number;
|
|
45
|
+
}
|
|
46
|
+
export interface RouteStat extends PerformanceTiming {
|
|
47
|
+
/** `GET /companies/:id`. */
|
|
48
|
+
key: string;
|
|
49
|
+
method: string;
|
|
50
|
+
route: string;
|
|
51
|
+
project?: string;
|
|
52
|
+
/** Answers with a 5xx status. */
|
|
53
|
+
errors: number;
|
|
54
|
+
/** Streaming responses: counted, not timed. */
|
|
55
|
+
streaming: number;
|
|
56
|
+
}
|
|
57
|
+
export interface SqlStat extends PerformanceTiming {
|
|
58
|
+
/** Its `sqlId`, else its text. */
|
|
59
|
+
key: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
project?: string;
|
|
62
|
+
op?: string;
|
|
63
|
+
table?: string;
|
|
64
|
+
/** The statement's text, its first 300 characters. */
|
|
65
|
+
text?: string;
|
|
66
|
+
failed: number;
|
|
67
|
+
}
|
|
68
|
+
export interface PageStat extends PerformanceTiming {
|
|
69
|
+
/** `<app> <pattern or path>`. */
|
|
70
|
+
key: string;
|
|
71
|
+
app: string;
|
|
72
|
+
path: string;
|
|
73
|
+
}
|
|
74
|
+
export type PerformanceBreachKind = 'slow-route' | 'slow-query' | 'n+1-query' | 'db-in-loop' | 'slow-page';
|
|
75
|
+
export interface PerformanceBreach {
|
|
76
|
+
/** Stable across runs of the same work: `slow-route:GET /x`, `n+1-query:GET /x:<sql>`. */
|
|
77
|
+
id: string;
|
|
78
|
+
kind: PerformanceBreachKind;
|
|
79
|
+
/** What broke the limit — a route, a statement, a page. */
|
|
80
|
+
subject: string;
|
|
81
|
+
/** What was measured, in words: `p95 812 ms, 4 of 9 runs at or above`. */
|
|
82
|
+
measured: string;
|
|
83
|
+
/** The limit, in words: `500 ms (slowRouteMs)`. */
|
|
84
|
+
limit: string;
|
|
85
|
+
/** Up to five event ids that show it. */
|
|
86
|
+
evidence: string[];
|
|
87
|
+
/** Waived by the person: why. A waived breach does not fail the window. */
|
|
88
|
+
waived?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface PerformanceReport {
|
|
91
|
+
window: {
|
|
92
|
+
from: number;
|
|
93
|
+
to: number;
|
|
94
|
+
};
|
|
95
|
+
/** The events the window measured (of the kinds above). */
|
|
96
|
+
events: number;
|
|
97
|
+
limits: PerformanceLimits;
|
|
98
|
+
/** `fail`: a breach stands; `unmeasured`: no request, statement or page ran in the window. */
|
|
99
|
+
verdict: 'pass' | 'fail' | 'unmeasured';
|
|
100
|
+
breaches: PerformanceBreach[];
|
|
101
|
+
waived: PerformanceBreach[];
|
|
102
|
+
/** Slowest p95 first. */
|
|
103
|
+
routes: RouteStat[];
|
|
104
|
+
sql: SqlStat[];
|
|
105
|
+
pages: PageStat[];
|
|
106
|
+
}
|
|
107
|
+
/** The limits for a workspace: its lens thresholds, then the team's own over them (both partial, both validated). */
|
|
108
|
+
export declare function performanceLimits(...overrides: Array<Partial<LensmcpThresholds> | null | undefined>): PerformanceLimits;
|
|
109
|
+
export declare class PerformanceWindow {
|
|
110
|
+
private readonly limits;
|
|
111
|
+
private readonly waivers;
|
|
112
|
+
private events;
|
|
113
|
+
private from;
|
|
114
|
+
private to;
|
|
115
|
+
private readonly routes;
|
|
116
|
+
private readonly sql;
|
|
117
|
+
private readonly pages;
|
|
118
|
+
/** Per open request: its statements' runs, until its `server-request` closes it. */
|
|
119
|
+
private readonly open;
|
|
120
|
+
/** A request's route once its `server-request` is seen. */
|
|
121
|
+
private readonly routeOf;
|
|
122
|
+
/** N+1 per route and statement: the worst count, and where. */
|
|
123
|
+
private readonly repeated;
|
|
124
|
+
private readonly loops;
|
|
125
|
+
constructor(limits: PerformanceLimits, waivers?: readonly PerformanceWaiver[]);
|
|
126
|
+
add(event: BaseEvent): void;
|
|
127
|
+
private request;
|
|
128
|
+
private statement;
|
|
129
|
+
/** A request ended (or the window did): its repeated statements become N+1 records. */
|
|
130
|
+
private close;
|
|
131
|
+
private loop;
|
|
132
|
+
private page;
|
|
133
|
+
/** The person's why for a breach, when a waiver matches what it is about; undefined: it stands. */
|
|
134
|
+
private waiverOf;
|
|
135
|
+
report(window?: {
|
|
136
|
+
from: number;
|
|
137
|
+
to: number;
|
|
138
|
+
}): PerformanceReport;
|
|
139
|
+
}
|
|
140
|
+
/** A report in a few lines — what the QA gate answers and the archive's performance.md opens with. */
|
|
141
|
+
export declare function describePerformance(report: PerformanceReport, max?: number): string;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var O=Object.defineProperty;var f=(s,t)=>O(s,"name",{value:t,configurable:!0});var I=Object.defineProperty,c=f((s,t)=>I(s,"name",{value:t,configurable:!0}),"p");import{resolveThresholds as R}from"./thresholds.js";export const PERFORMANCE_EVENT_KINDS=["server-request","db-query","loop","page-visit"];export function performanceLimits(...s){let t={};for(const r of s)r&&(t={...t,...k(r)});const e=R(t);return{slowRouteMs:e.slowRouteMs,slowQueryMs:e.slowQueryMs,nPlusOne:e.nPlusOne,dbInLoop:e.dbInLoop,slowPageMs:e.slowPageMs}}f(performanceLimits,"performanceLimits"),c(performanceLimits,"performanceLimits");function k(s){const t={};for(const[e,r]of Object.entries(s))typeof r=="number"&&Number.isFinite(r)&&r>=0&&(t[e]=r);return t}f(k,"P"),c(k,"resolveThresholdsOnly");const $=c(s=>s&&typeof s=="object"?s:{},"obj"),m=c(s=>typeof s=="string"&&s?s:void 0,"str"),w=c(s=>typeof s=="number"&&Number.isFinite(s)?s:void 0,"num"),L=2e3,y=5e3,P=2e4,x=c(()=>({count:0,times:[],slow:0,max:0,evidence:[]}),"newTiming");function q(s,t,e,r){if(s.max=Math.max(s.max,t),t>=e&&(s.slow+=1,s.evidence.length<5&&s.evidence.push(r)),s.times.length<L)s.times.push(t);else{let n=0;for(let a=1;a<s.times.length;a++)s.times[a]<s.times[n]&&(n=a);t>s.times[n]&&(s.times[n]=t)}}f(q,"q"),c(q,"time");function j(s,t){return s.length?s[Math.min(s.length-1,Math.floor(s.length*t))]:0}f(j,"k"),c(j,"percentile");function M(s){const t=[...s.times].sort((e,r)=>e-r);return{count:s.count,timed:s.times.length,p50Ms:Math.round(j(t,.5)),p95Ms:Math.round(j(t,.95)),maxMs:Math.round(s.max),slow:s.slow}}f(M,"v"),c(M,"timing");function v(s,t,e){return s.includes("*")?new RegExp(`^${s.split("*").map(r=>r.replace(/[.+?^${}()|[\]\\]/g,"\\$&")).join(".*")}$`).test(t):e?t===s:t.includes(s)}f(v,"w"),c(v,"matches");export class PerformanceWindow{static{f(this,"PerformanceWindow")}static{c(this,"PerformanceWindow")}constructor(t,e=[]){this.events=0,this.from=1/0,this.to=0,this.routes=new Map,this.sql=new Map,this.pages=new Map,this.open=new Map,this.routeOf=new Map,this.repeated=new Map,this.loops=[],this.limits=t,this.waivers=e.filter(r=>typeof r?.why=="string"&&(r.route||r.sql||r.page))}add(t){const e=$(t.raw),r=e.kind;if(r!=="server-request"&&r!=="db-query"&&r!=="loop"&&r!=="page-visit")return;this.events+=1,this.from=Math.min(this.from,t.timestamp),this.to=Math.max(this.to,t.timestamp);const n=$(t.context),a=m(n.projectName)??m(e.project),l=m(n.requestId);r==="server-request"?this.request(t,e,a,l):r==="db-query"?this.statement(t,e,a,l):r==="loop"?this.loop(t,e,l):this.page(t,e)}request(t,e,r,n){const a=$(e.request),l=m(a.method)??"?",p=m(a.route)??"?",u=`${l} ${p}`;let h=this.routes.get(u);if(!h){if(this.routes.size>=y)return;h={...x(),method:l,route:p,...r?{project:r}:{},errors:0,streaming:0},this.routes.set(u,h)}h.count+=1;const g=w(a.status);g!==void 0&&g>=500&&(h.errors+=1);const d=w(a.durationMs);a.streaming===!0?h.streaming+=1:d!==void 0&&q(h,d,this.limits.slowRouteMs,t.id),n&&(this.routeOf.set(n,u),this.routeOf.size>P&&this.routeOf.delete(this.routeOf.keys().next().value),this.close(n))}statement(t,e,r,n){const a=m(e.statement)??m(e.query),l=m(e.sqlId)??a?.slice(0,300);if(!l)return;const p=1+Math.max(0,w(e.floodSuppressed)??0);let u=this.sql.get(l);if(!u&&this.sql.size<y){u={...x(),failed:0};const d=m(e.sqlName),o=m(e.op),i=m(e.table);d&&(u.name=d),r&&(u.project=r),o&&(u.op=o),i&&(u.table=i),this.sql.set(l,u)}if(u){u.count+=p,!u.text&&a&&(u.text=a.slice(0,300)),e.error!==void 0&&e.error!==null&&(u.failed+=1);const d=w(e.durationMs);d!==void 0&&q(u,d,this.limits.slowQueryMs,t.id)}if(!n)return;let h=this.open.get(n);h||(this.open.size>=P&&this.close(this.open.keys().next().value),h=new Map,this.open.set(n,h));const g=h.get(l);g?g.n+=p:h.set(l,{n:p,first:t.id})}close(t){const e=this.open.get(t);if(!e)return;this.open.delete(t);const r=this.routeOf.get(t)??"a request with no route seen";for(const[n,{n:a,first:l}]of e){if(a<this.limits.nPlusOne)continue;const p=`${r}\0${n}`,u=this.repeated.get(p)??{route:r,sql:n,worst:0,requests:0,evidence:[]};u.worst=Math.max(u.worst,a),u.requests+=1,u.evidence.length<5&&u.evidence.push(l),this.repeated.set(p,u)}}loop(t,e,r){const n=$(e.loop),a=w(n.dbCallsInsideLoop)??0;if(a<=this.limits.dbInLoop)return;const l=(r&&this.routeOf.get(r))??t.title??"a loop";this.loops.length>=200||this.loops.push({id:`db-in-loop:${l}`,kind:"db-in-loop",subject:l,measured:`${a} DB calls inside one loop (${w(n.iterations)??"?"} iterations)`,limit:`more than ${this.limits.dbInLoop} (dbInLoop)`,evidence:[t.id]})}page(t,e){if(e.phase!=="ready"||e.cause==="hmr")return;const r=m(e.app)??"?",n=m(e.pattern)??m(e.path)??"?",a=`${r} ${n}`;let l=this.pages.get(a);if(!l){if(this.pages.size>=y)return;l={...x(),app:r,path:n},this.pages.set(a,l)}l.count+=1;const p=e.nav==="hard"?w(e.appReadyMs)??w(e.readyMs):w(e.readyMs);p!==void 0&&q(l,p,this.limits.slowPageMs,t.id)}waiverOf(t){for(const e of this.waivers)if(e.route&&t.route&&v(e.route,t.route,!0)||e.sql&&t.sql&&(v(e.sql,t.sql,!1)||t.sqlText!==void 0&&v(e.sql,t.sqlText,!1))||e.page&&t.page&&v(e.page,t.page,!0))return e.why}report(t){for(const o of[...this.open.keys()])this.close(o);const e=this.limits,r=[],n=c((o,i="runs")=>`p95 ${o.p95Ms} ms, max ${o.maxMs} ms \u2014 ${o.slow} of ${o.timed} ${i} at or above`,"slowWords"),a=[...this.routes.entries()].map(([o,i])=>({key:o,method:i.method,route:i.route,...i.project?{project:i.project}:{},errors:i.errors,streaming:i.streaming,...M(i),evidence:i.evidence}));for(const o of a)o.slow>1&&r.push({breach:{id:`slow-route:${o.key}`,kind:"slow-route",subject:o.key,measured:n(o),limit:`${e.slowRouteMs} ms (slowRouteMs)`,evidence:o.evidence},about:{route:o.key}});const l=[...this.sql.entries()].map(([o,i])=>({key:o,...i.name?{name:i.name}:{},...i.project?{project:i.project}:{},...i.op?{op:i.op}:{},...i.table?{table:i.table}:{},...i.text?{text:i.text}:{},failed:i.failed,...M(i),evidence:i.evidence}));for(const o of l)o.slow>1&&r.push({breach:{id:`slow-query:${o.key}`,kind:"slow-query",subject:o.name??o.key,measured:n(o),limit:`${e.slowQueryMs} ms (slowQueryMs)`,evidence:o.evidence},about:{sql:o.key,...o.text?{sqlText:o.text}:{}}});for(const o of this.repeated.values()){const i=this.sql.get(o.sql);r.push({breach:{id:`n+1-query:${o.route}:${o.sql}`,kind:"n+1-query",subject:`${o.route} runs ${i?.name??o.sql}`,measured:`up to ${o.worst}\xD7 in one request (${o.requests} request${o.requests===1?"":"s"})`,limit:`fewer than ${e.nPlusOne} (nPlusOne)`,evidence:o.evidence},about:{route:o.route,sql:o.sql,...i?.text?{sqlText:i.text}:{}}})}for(const o of this.loops)r.push({breach:o,about:{route:o.subject}});const p=[...this.pages.entries()].map(([o,i])=>({key:o,app:i.app,path:i.path,...M(i),evidence:i.evidence}));for(const o of p)o.slow>1&&r.push({breach:{id:`slow-page:${o.key}`,kind:"slow-page",subject:o.key,measured:n(o,"visits"),limit:`${e.slowPageMs} ms (slowPageMs)`,evidence:o.evidence},about:{page:o.path}});const u=[],h=[];for(const{breach:o,about:i}of r){const b=this.waiverOf(i);b?h.push({...o,waived:b}):u.push(o)}const g=c(o=>{const{evidence:i,...b}=o;return b},"strip"),d=this.routes.size+this.sql.size+this.pages.size>0;return{window:t??{from:Number.isFinite(this.from)?this.from:0,to:this.to},events:this.events,limits:{...e},verdict:u.length?"fail":d?"pass":"unmeasured",breaches:u,waived:h,routes:a.map(g).sort((o,i)=>i.p95Ms-o.p95Ms),sql:l.map(g).sort((o,i)=>i.p95Ms-o.p95Ms),pages:p.map(g).sort((o,i)=>i.p95Ms-o.p95Ms)}}}export function describePerformance(s,t=12){const e=s.limits,r=[s.verdict==="unmeasured"?"Performance: unmeasured \u2014 no request, statement or page ran in the window.":`Performance: ${s.verdict==="pass"?"PASS":`FAIL \u2014 ${s.breaches.length} breach${s.breaches.length===1?"":"es"}`} over ${s.routes.length} route${s.routes.length===1?"":"s"}, ${s.sql.length} statement${s.sql.length===1?"":"s"}, ${s.pages.length} page${s.pages.length===1?"":"s"} (limits: route ${e.slowRouteMs} ms, query ${e.slowQueryMs} ms, N+1 ${e.nPlusOne}, DB in a loop ${e.dbInLoop}, page ${e.slowPageMs} ms).`];for(const n of s.breaches.slice(0,t))r.push(`- ${n.kind} \xB7 ${n.subject}: ${n.measured}; limit ${n.limit}`);return s.breaches.length>t&&r.push(`- \u2026 ${s.breaches.length-t} more`),s.waived.length&&r.push(`Waived by .lensmcp/team/performance.json: ${s.waived.map(n=>`${n.kind} \xB7 ${n.subject} (${n.waived})`).slice(0,t).join("; ")}`),r.join(`
|
|
2
|
+
`)}f(describePerformance,"describePerformance"),c(describePerformance,"describePerformance");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.41",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@lensmcp/protocol-types": "1.25.
|
|
87
|
+
"@lensmcp/protocol-types": "1.25.41",
|
|
88
88
|
"tslib": "^2.3.0"
|
|
89
89
|
},
|
|
90
90
|
"license": "Apache-2.0",
|