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:
parent
09b320386b
commit
878876c7af
7 changed files with 433 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue