Files
lislgosms/tools/testing/benchmark-signature-analytics.mjs

144 lines
5.8 KiB
JavaScript

import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { execFileSync } from 'node:child_process';
import Module from 'node:module';
import { randomUUID } from 'node:crypto';
const require = createRequire(new URL('../../api/package.json', import.meta.url));
const url = new URL(process.env.SIGNATURE_TEST_DATABASE_URL || '');
assert(url.hostname === '127.0.0.1' && url.pathname.startsWith('/cmpp_qa_signature_'));
process.env.DATABASE_URL = url.toString();
process.env.NODE_ENV = 'test';
require('reflect-metadata');
const { PrismaService } = require('./dist/prisma/prisma.service'),
{ Prisma } = require('@prisma/client');
const { activityCounts } = require('./dist/signature-analytics/analytics-aggregate');
const { todayKey, addDays, startOfDay } = require('./dist/signature-analytics/analytics-date');
const oldSource = execFileSync(
'git',
['show', '4eb7b16d122da14f921093716d4ca1ed390d9e4c:api/src/signature-retirement/signature-retirement.service.ts'],
{ encoding: 'utf8' },
);
const compiled = require('typescript').transpileModule(oldSource, {
compilerOptions: { module: 1, target: 9, experimentalDecorators: true, emitDecoratorMetadata: true },
}).outputText;
const oldModule = new Module(require.resolve('./dist/signature-retirement/signature-retirement.service'));
oldModule.filename = require.resolve('./dist/signature-retirement/signature-retirement.service');
oldModule.paths = require.resolve.paths('@prisma/client');
oldModule._compile(compiled, oldModule.filename);
const db = new PrismaService(),
prefix = 'bench' + randomUUID().slice(0, 8),
N = 300,
K = 100,
D = addDays(todayKey(), -1),
start = startOfDay(D),
end = startOfDay(todayKey());
let last;
const traced = new Proxy(db, {
get(t, k) {
if (k === '$queryRaw')
return async (q) => {
last = q;
return db.$queryRaw(q);
};
const v = t[k];
return typeof v === 'function' ? v.bind(t) : v;
},
});
try {
const tenant = await db.tenant.create({ data: { name: prefix, code: prefix } }),
app = await db.smsApplication.create({
data: {
tenantId: tenant.id,
name: prefix,
cmppAccount: prefix,
cmppEnterpriseCode: '000001',
secretHash: 'isolated',
interfaceEnabled: false,
},
}),
channel = await db.smsChannel.create({
data: {
name: prefix,
code: prefix,
srcId: '1069',
gatewayHost: '127.0.0.1',
gatewayPort: 1,
account: 'isolated',
passwordCipher: 'not-a-secret',
carriers: ['mobile'],
status: 'disabled',
},
});
await db.$executeRaw`INSERT INTO "SmsSignature"(id,"tenantId","applicationId",name,"updatedAt") SELECT ${prefix}||n,${tenant.id},${app.id},${prefix}||n,NOW() FROM generate_series(1,${N}) n`;
await db.$executeRaw`INSERT INTO "SmsMessageRecord"(id,"messageId","tenantId","applicationId","signatureId","phoneNumber",content,carrier,"queuedAt","billingUnits","updatedAt") SELECT ${prefix}||n||'-'||k,${prefix}||n||'-'||k,${tenant.id},${app.id},${prefix}||n,'13800000200','isolated','mobile',${start},1,NOW() FROM generate_series(1,${N}) n CROSS JOIN generate_series(1,${K}) k`;
await db.$executeRaw`INSERT INTO "SmsSubmitRecord"(id,"messageRecordId","submitId","channelId","submitStatus","submittedAt","createdAt","updatedAt") SELECT id,id,id,${channel.id},'accepted',${start},${start},NOW() FROM "SmsMessageRecord" WHERE "tenantId"=${tenant.id}`;
await db.$executeRawUnsafe('ANALYZE "SmsMessageRecord"');
await db.$executeRawUnsafe('ANALYZE "SmsSubmitRecord"');
const dims = Array.from({ length: N }, (_, i) =>
['enterprise', 'channel'].map((type) => ({
dimensionKey: JSON.stringify([type, prefix + (i + 1)]),
dimensionType: type,
tenantId: tenant.id,
applicationId: app.id,
signatureId: prefix + (i + 1),
channelKey: type === 'channel' ? channel.id : '',
channelId: type === 'channel' ? channel.id : null,
carrier: 'mobile',
approvedAt: startOfDay(addDays(D, -60)),
signatureName: prefix + (i + 1),
tenantName: prefix,
applicationName: prefix,
channelName: prefix,
})),
).flat();
const old = new oldModule.exports.SignatureRetirementService(traced);
const before = performance.now();
for (const d of dims) {
const r = await old.activityCounts(d, start, end);
assert.equal(r.acceptedBusinessCount, K);
}
const oldMs = performance.now() - before;
const sampleOld = last;
const now = performance.now(),
counts = await activityCounts(traced, dims, D),
newMs = performance.now() - now,
sampleNew = last;
for (const d of dims) assert.equal(counts.get(d.dimensionKey).acceptedBusinessCount, K);
const explain = async (q) =>
(await db.$queryRaw(Prisma.sql`EXPLAIN (ANALYZE,BUFFERS,FORMAT JSON) ${q}`))[0]['QUERY PLAN'][0];
const oldPlan = await explain(sampleOld),
newPlan = await explain(sampleNew);
const scanRows = (p) => {
let sum = 0;
const walk = (n) => {
if (
n['Node Type']?.includes('Scan') &&
['SmsSubmitRecord', 'SmsMessageRecord', 'SmsReceiptRecord', 'SmsMessageSegmentAudit'].includes(
n['Relation Name'],
)
)
sum += (n['Actual Rows'] + (n['Rows Removed by Filter'] || 0)) * (n['Actual Loops'] || 0);
for (const c of n.Plans || []) walk(c);
};
walk(p.Plan);
return sum;
};
console.log(
JSON.stringify({
messages: N * K,
dimensions: dims.length,
oldMs,
newMs,
reduction: 1 - newMs / oldMs,
oldSampleRows: scanRows(oldPlan),
estimatedOldRows: scanRows(oldPlan) * dims.length,
newRows: scanRows(newPlan),
note: 'warm cache, per-dimension sample extrapolation; not production CPU or whole job capacity',
oldPlan,
newPlan,
}),
);
} finally {
await db.onModuleDestroy();
}