java-topology/defects/ghost/patch/ghost-0001-referrers-history-source-date-map.patch
russell@unturf.com 878876c7af
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.
2026-04-25 15:01:06 -04:00

58 lines
2.7 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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);
}
});