@lensmcp/core 1.25.41 → 1.25.43
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.
|
@@ -17,8 +17,14 @@ import { type LensmcpThresholds } from './thresholds.js';
|
|
|
17
17
|
* server reloaded (`cause: 'hmr'`) is left out, and a hard load is timed from the app's entry (`appReadyMs`), not from
|
|
18
18
|
* the dev server serving it.
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* A service's record of the statement runs its flood guard kept off the bus (node-instrumentation's `sql-drops.ts`,
|
|
22
|
+
* the same name there): per statement, how many, how many slow (at or over the record's `slowMs`), how many failed, the
|
|
23
|
+
* slowest. A window counts them as runs it knows of but did not time (issues/closed/foodguard-141).
|
|
24
|
+
*/
|
|
25
|
+
export declare const SQL_DROPS_KIND = "db-dropped";
|
|
20
26
|
/** 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"];
|
|
27
|
+
export declare const PERFORMANCE_EVENT_KINDS: readonly ["server-request", "db-query", "db-dropped", "loop", "page-visit"];
|
|
22
28
|
/** The limits a window is judged by: the workspace's lens thresholds, for the kinds measured here. */
|
|
23
29
|
export type PerformanceLimits = Pick<LensmcpThresholds, 'slowRouteMs' | 'slowQueryMs' | 'nPlusOne' | 'dbInLoop' | 'slowPageMs'>;
|
|
24
30
|
/**
|
|
@@ -64,6 +70,8 @@ export interface SqlStat extends PerformanceTiming {
|
|
|
64
70
|
/** The statement's text, its first 300 characters. */
|
|
65
71
|
text?: string;
|
|
66
72
|
failed: number;
|
|
73
|
+
/** Runs known only from the service's drop record — counted in `count`, `slow`, `failed` and `maxMs`, never timed. */
|
|
74
|
+
dropped?: number;
|
|
67
75
|
}
|
|
68
76
|
export interface PageStat extends PerformanceTiming {
|
|
69
77
|
/** `<app> <pattern or path>`. */
|
|
@@ -99,6 +107,15 @@ export interface PerformanceReport {
|
|
|
99
107
|
verdict: 'pass' | 'fail' | 'unmeasured';
|
|
100
108
|
breaches: PerformanceBreach[];
|
|
101
109
|
waived: PerformanceBreach[];
|
|
110
|
+
/**
|
|
111
|
+
* Statement runs the services' flood guards kept off the bus, from their drop records: counted, never timed. A pass
|
|
112
|
+
* over a window with dropped runs says so — its percentiles are of the runs that reached the bus.
|
|
113
|
+
*/
|
|
114
|
+
dropped?: {
|
|
115
|
+
runs: number;
|
|
116
|
+
slow: number;
|
|
117
|
+
failed: number;
|
|
118
|
+
};
|
|
102
119
|
/** Slowest p95 first. */
|
|
103
120
|
routes: RouteStat[];
|
|
104
121
|
sql: SqlStat[];
|
|
@@ -114,6 +131,7 @@ export declare class PerformanceWindow {
|
|
|
114
131
|
private to;
|
|
115
132
|
private readonly routes;
|
|
116
133
|
private readonly sql;
|
|
134
|
+
private readonly drops;
|
|
117
135
|
private readonly pages;
|
|
118
136
|
/** Per open request: its statements' runs, until its `server-request` closes it. */
|
|
119
137
|
private readonly open;
|
|
@@ -125,7 +143,16 @@ export declare class PerformanceWindow {
|
|
|
125
143
|
constructor(limits: PerformanceLimits, waivers?: readonly PerformanceWaiver[]);
|
|
126
144
|
add(event: BaseEvent): void;
|
|
127
145
|
private request;
|
|
146
|
+
/** The statement `key`'s entry, made from what `raw` says of it; undefined past the cap. */
|
|
147
|
+
private sqlEntry;
|
|
128
148
|
private statement;
|
|
149
|
+
/**
|
|
150
|
+
* A service's drop record: each statement's dropped runs are counted — runs, failures, the slowest — but not timed. Its
|
|
151
|
+
* slow runs are slow by the window's limit when that is at or under the record's `slowMs`; the slowest run says at
|
|
152
|
+
* least one was when it is at or over the limit. A floor either way: a run between a lower limit and `slowMs` is not
|
|
153
|
+
* told apart.
|
|
154
|
+
*/
|
|
155
|
+
private dropped;
|
|
129
156
|
/** A request ended (or the window did): its repeated statements become N+1 records. */
|
|
130
157
|
private close;
|
|
131
158
|
private loop;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`)}
|
|
1
|
+
"use strict";var L=Object.defineProperty;var v=(o,t)=>L(o,"name",{value:t,configurable:!0});var N=Object.defineProperty,f=v((o,t)=>N(o,"name",{value:t,configurable:!0}),"f");import{resolveThresholds as E}from"./thresholds.js";export const SQL_DROPS_KIND="db-dropped",PERFORMANCE_EVENT_KINDS=["server-request","db-query",SQL_DROPS_KIND,"loop","page-visit"];export function performanceLimits(...o){let t={};for(const r of o)r&&(t={...t,...I(r)});const e=E(t);return{slowRouteMs:e.slowRouteMs,slowQueryMs:e.slowQueryMs,nPlusOne:e.nPlusOne,dbInLoop:e.dbInLoop,slowPageMs:e.slowPageMs}}v(performanceLimits,"performanceLimits"),f(performanceLimits,"performanceLimits");function I(o){const t={};for(const[e,r]of Object.entries(o))typeof r=="number"&&Number.isFinite(r)&&r>=0&&(t[e]=r);return t}v(I,"S"),f(I,"resolveThresholdsOnly");const $=f(o=>o&&typeof o=="object"?o:{},"obj"),m=f(o=>typeof o=="string"&&o?o:void 0,"str"),c=f(o=>typeof o=="number"&&Number.isFinite(o)?o:void 0,"num"),Q=2e3,j=5e3,R=2e4,k=f(()=>({count:0,times:[],slow:0,max:0,evidence:[]}),"newTiming");function q(o,t,e,r){if(o.max=Math.max(o.max,t),t>=e&&(o.slow+=1,o.evidence.length<5&&o.evidence.push(r)),o.times.length<Q)o.times.push(t);else{let n=0;for(let a=1;a<o.times.length;a++)o.times[a]<o.times[n]&&(n=a);t>o.times[n]&&(o.times[n]=t)}}v(q,"v"),f(q,"time");function P(o,t){return o.length?o[Math.min(o.length-1,Math.floor(o.length*t))]:0}v(P,"P"),f(P,"percentile");function x(o){const t=[...o.times].sort((e,r)=>e-r);return{count:o.count,timed:o.times.length,p50Ms:Math.round(P(t,.5)),p95Ms:Math.round(P(t,.95)),maxMs:Math.round(o.max),slow:o.slow}}v(x,"k"),f(x,"timing");function b(o,t,e){return o.includes("*")?new RegExp(`^${o.split("*").map(r=>r.replace(/[.+?^${}()|[\]\\]/g,"\\$&")).join(".*")}$`).test(t):e?t===o:t.includes(o)}v(b,"y"),f(b,"matches");export class PerformanceWindow{static{v(this,"PerformanceWindow")}static{f(this,"PerformanceWindow")}constructor(t,e=[]){this.events=0,this.from=1/0,this.to=0,this.routes=new Map,this.sql=new Map,this.drops={runs:0,slow:0,failed:0},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!==SQL_DROPS_KIND&&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===SQL_DROPS_KIND?this.dropped(t,e,a):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>=j)return;h={...k(),method:l,route:p,...r?{project:r}:{},errors:0,streaming:0},this.routes.set(u,h)}h.count+=1;const d=c(a.status);d!==void 0&&d>=500&&(h.errors+=1);const g=c(a.durationMs);a.streaming===!0?h.streaming+=1:g!==void 0&&q(h,g,this.limits.slowRouteMs,t.id),n&&(this.routeOf.set(n,u),this.routeOf.size>R&&this.routeOf.delete(this.routeOf.keys().next().value),this.close(n))}sqlEntry(t,e,r){let n=this.sql.get(t);if(!n&&this.sql.size<j){n={...k(),failed:0,dropped:0};const a=m(e.sqlName),l=m(e.op),p=m(e.table);a&&(n.name=a),r&&(n.project=r),l&&(n.op=l),p&&(n.table=p),this.sql.set(t,n)}return 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,c(e.floodSuppressed)??0),u=this.sqlEntry(l,e,r);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 g=c(e.durationMs);g!==void 0&&q(u,g,this.limits.slowQueryMs,t.id)}if(!n)return;let h=this.open.get(n);h||(this.open.size>=R&&this.close(this.open.keys().next().value),h=new Map,this.open.set(n,h));const d=h.get(l);d?d.n+=p:h.set(l,{n:p,first:t.id})}dropped(t,e,r){const n=c(e.slowMs)??-1/0,a=f((h,d)=>Math.max(this.limits.slowQueryMs<=n?Math.max(0,c(h.slow)??0):0,d>=this.limits.slowQueryMs?1:0),"slowOf"),l=Array.isArray(e.shapes)?e.shapes:[];for(const h of l){const d=$(h),g=m(d.sqlId),s=Math.max(0,c(d.runs)??0);if(!g||!s)continue;const i=Math.max(0,c(d.maxMs)??0),M=Math.max(0,c(d.failed)??0),y=a(d,i);this.drops.runs+=s,this.drops.slow+=y,this.drops.failed+=M;const w=this.sqlEntry(g,d,r);if(w){if(w.count+=s,w.dropped+=s,w.failed+=M,w.max=Math.max(w.max,i),!w.text){const O=m(d.query);O&&(w.text=O.slice(0,300))}y&&(w.slow+=y,w.evidence.length<5&&w.evidence.push(t.id))}}const p=$(e.overflow),u=Math.max(0,c(p.runs)??0);if(u){const h=Math.max(0,c(p.maxMs)??0);this.drops.runs+=u,this.drops.slow+=a(p,h),this.drops.failed+=Math.max(0,c(p.failed)??0)}}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=c(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 (${c(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>=j)return;l={...k(),app:r,path:n},this.pages.set(a,l)}l.count+=1;const p=e.nav==="hard"?c(e.appReadyMs)??c(e.readyMs):c(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&&b(e.route,t.route,!0)||e.sql&&t.sql&&(b(e.sql,t.sql,!1)||t.sqlText!==void 0&&b(e.sql,t.sqlText,!1))||e.page&&t.page&&b(e.page,t.page,!0))return e.why}report(t){for(const s of[...this.open.keys()])this.close(s);const e=this.limits,r=[],n=f((s,i="runs")=>`p95 ${s.p95Ms} ms, max ${s.maxMs} ms \u2014 ${s.slow} of ${s.timed} ${i} at or above`,"slowWords"),a=[...this.routes.entries()].map(([s,i])=>({key:s,method:i.method,route:i.route,...i.project?{project:i.project}:{},errors:i.errors,streaming:i.streaming,...x(i),evidence:i.evidence}));for(const s of a)s.slow>1&&r.push({breach:{id:`slow-route:${s.key}`,kind:"slow-route",subject:s.key,measured:n(s),limit:`${e.slowRouteMs} ms (slowRouteMs)`,evidence:s.evidence},about:{route:s.key}});const l=[...this.sql.entries()].map(([s,i])=>({key:s,...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,...i.dropped?{dropped:i.dropped}:{},...x(i),evidence:i.evidence}));for(const s of l){const i=s.dropped?`p95 ${s.p95Ms} ms, max ${s.maxMs} ms \u2014 ${s.slow} of ${s.count} runs at or above (${s.dropped} counted from the service's drop record, not timed)`:n(s);s.slow>1&&r.push({breach:{id:`slow-query:${s.key}`,kind:"slow-query",subject:s.name??s.key,measured:i,limit:`${e.slowQueryMs} ms (slowQueryMs)`,evidence:s.evidence},about:{sql:s.key,...s.text?{sqlText:s.text}:{}}})}for(const s of this.repeated.values()){const i=this.sql.get(s.sql);r.push({breach:{id:`n+1-query:${s.route}:${s.sql}`,kind:"n+1-query",subject:`${s.route} runs ${i?.name??s.sql}`,measured:`up to ${s.worst}\xD7 in one request (${s.requests} request${s.requests===1?"":"s"})`,limit:`fewer than ${e.nPlusOne} (nPlusOne)`,evidence:s.evidence},about:{route:s.route,sql:s.sql,...i?.text?{sqlText:i.text}:{}}})}for(const s of this.loops)r.push({breach:s,about:{route:s.subject}});const p=[...this.pages.entries()].map(([s,i])=>({key:s,app:i.app,path:i.path,...x(i),evidence:i.evidence}));for(const s of p)s.slow>1&&r.push({breach:{id:`slow-page:${s.key}`,kind:"slow-page",subject:s.key,measured:n(s,"visits"),limit:`${e.slowPageMs} ms (slowPageMs)`,evidence:s.evidence},about:{page:s.path}});const u=[],h=[];for(const{breach:s,about:i}of r){const M=this.waiverOf(i);M?h.push({...s,waived:M}):u.push(s)}const d=f(s=>{const{evidence:i,...M}=s;return M},"strip"),g=this.routes.size+this.sql.size+this.pages.size+this.drops.runs>0;return{window:t??{from:Number.isFinite(this.from)?this.from:0,to:this.to},events:this.events,limits:{...e},verdict:u.length?"fail":g?"pass":"unmeasured",breaches:u,waived:h,...this.drops.runs?{dropped:{...this.drops}}:{},routes:a.map(d).sort((s,i)=>i.p95Ms-s.p95Ms),sql:l.map(d).sort((s,i)=>i.p95Ms-s.p95Ms),pages:p.map(d).sort((s,i)=>i.p95Ms-s.p95Ms)}}}export function describePerformance(o,t=12){const e=o.limits,r=[o.verdict==="unmeasured"?"Performance: unmeasured \u2014 no request, statement or page ran in the window.":`Performance: ${o.verdict==="pass"?"PASS":`FAIL \u2014 ${o.breaches.length} breach${o.breaches.length===1?"":"es"}`} over ${o.routes.length} route${o.routes.length===1?"":"s"}, ${o.sql.length} statement${o.sql.length===1?"":"s"}, ${o.pages.length} page${o.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).`],n=o.dropped;n&&r.push(`${n.runs} statement run${n.runs===1?"":"s"} reached the lens only as counts (the services' flood guards): ${n.slow} slow, ${n.failed} failed \u2014 counted above, not in the percentiles.`);for(const a of o.breaches.slice(0,t))r.push(`- ${a.kind} \xB7 ${a.subject}: ${a.measured}; limit ${a.limit}`);return o.breaches.length>t&&r.push(`- \u2026 ${o.breaches.length-t} more`),o.waived.length&&r.push(`Waived by .lensmcp/team/performance.json: ${o.waived.map(a=>`${a.kind} \xB7 ${a.subject} (${a.waived})`).slice(0,t).join("; ")}`),r.join(`
|
|
2
|
+
`)}v(describePerformance,"describePerformance"),f(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.43",
|
|
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.43",
|
|
88
88
|
"tslib": "^2.3.0"
|
|
89
89
|
},
|
|
90
90
|
"license": "Apache-2.0",
|