wave16: ghost-0001 UNDF-1302 (15x-184x ReferrersStats) + 6 clean-scan additions

Flagship: Ghost ReferrersStatsService.getReferrersHistory Array.find with
multi-key predicate per paid conversion (O(P*A) -> O(P+A)). Long-running
Ghost sites with 200+ referrers x year of dates hit 4 second dashboard
loads; Map<source|date,entry> hoist gives 184x speedup at A=110k P=1k.

Wave 16 honor roll: fastify, samtools, argo-workflows, cypress, bitcoin,
strapi. Cumulative: 80 projects.
This commit is contained in:
russell@unturf.com 2026-04-25 15:01:06 -04:00
parent 09b320386b
commit 878876c7af
No known key found for this signature in database
7 changed files with 433 additions and 0 deletions

View file

@ -0,0 +1,58 @@
# UNDF: UNDF-2026-000001302
# CWE-407: Algorithmic Complexity — O(P×A) → O(P+A) in ReferrersStatsService.getReferrersHistory
#
# Defect: ghost/core/core/server/services/stats/referrers-stats-service.js
# merges paid-conversion events into a base list of signup events keyed by
# (source, date). The merge does:
# paidConversionEntries.forEach(entry => {
# const existing = allEntries.find(e => e.source === entry.source && e.date === entryDate);
# if (existing) existing.paid_conversions = entry.paid_conversions;
# else allEntries.push(...);
# });
# Per paid conversion, Array.find is an O(A) linear scan over allEntries.
# Total cost: O(P × A) where A scales with (referral sources) × (date range).
#
# Real-world scale: a Ghost site running for a year with 200 referral sources
# and 365 dates has A ≈ 70k, with hundreds of paid conversions per refresh.
# The referrers dashboard then issues 7M+ membership comparisons per load.
#
# Fix: Build a Map<"source|date", entry> from allEntries once. Per-paid-conversion
# lookup drops from O(A) to O(1). Total cost: O(P + A).
#
# Complexity gate (defects/ghost/bench/bench-ghost-0001.py):
# A=10k P=200: defective ~25ms, fixed <1ms (>=20× speedup)
# k-scaling 5×: time ratio must be <17.5×
--- a/ghost/core/core/server/services/stats/referrers-stats-service.js
+++ b/ghost/core/core/server/services/stats/referrers-stats-service.js
@@ -144,11 +144,18 @@ class ReferrersStatsService {
};
});
+ // Build a (source|date) -> entry lookup so per-paid-conversion membership
+ // is O(1) instead of an O(A) Array.find scan. The dashboard renders all
+ // signup+conversion entries; for sites with many referrers and a long
+ // date range, A grows quickly.
+ const allEntriesByKey = new Map();
+ for (const e of allEntries) {
+ allEntriesByKey.set(`${e.source}|${e.date}`, e);
+ }
+
paidConversionEntries.forEach((entry) => {
const entryDate = moment(entry.date).format('YYYY-MM-DD');
- const existingEntry = allEntries.find(e => e.source === entry.source && e.date === entryDate);
-
+ const existingEntry = allEntriesByKey.get(`${entry.source}|${entryDate}`);
if (existingEntry) {
existingEntry.paid_conversions = entry.paid_conversions;
} else {
- allEntries.push({
+ const newEntry = {
...entry,
signups: 0,
date: entryDate
- });
+ };
+ allEntries.push(newEntry);
+ allEntriesByKey.set(`${entry.source}|${entryDate}`, newEntry);
}
});