feat(statistics): attribute rolling traffic to accounts
This commit is contained in:
@@ -79,12 +79,13 @@ import { mergeKnownTotalBytes } from "./download-size";
|
|||||||
import { DiskCapacityError, DiskReservationCoordinator, type DiskReservationLease } from "./disk-space";
|
import { DiskCapacityError, DiskReservationCoordinator, type DiskReservationLease } from "./disk-space";
|
||||||
import { createRendererState } from "./renderer-state";
|
import { createRendererState } from "./renderer-state";
|
||||||
import {
|
import {
|
||||||
|
RollingAccountStatisticsAccumulator,
|
||||||
addStatisticsActiveIntervalInPlace,
|
addStatisticsActiveIntervalInPlace,
|
||||||
addStatisticsBytesInPlace,
|
addStatisticsBytesInPlace,
|
||||||
addStatisticsOutcomeInPlace,
|
addStatisticsOutcomeInPlace,
|
||||||
createStatisticsLedger,
|
createStatisticsLedger,
|
||||||
loadStatisticsLedger,
|
loadStatisticsLedger,
|
||||||
normalizeStatisticsLedger,
|
projectStatisticsLedger,
|
||||||
saveStatisticsLedger,
|
saveStatisticsLedger,
|
||||||
seedStatisticsDayProviderBytes
|
seedStatisticsDayProviderBytes
|
||||||
} from "./statistics-ledger";
|
} from "./statistics-ledger";
|
||||||
@@ -1831,6 +1832,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private statisticsLedger: StatisticsLedger;
|
private statisticsLedger: StatisticsLedger;
|
||||||
|
|
||||||
|
private rollingAccountStatistics: RollingAccountStatisticsAccumulator;
|
||||||
|
|
||||||
private statisticsDirty = false;
|
private statisticsDirty = false;
|
||||||
|
|
||||||
private statisticsUrgent = false;
|
private statisticsUrgent = false;
|
||||||
@@ -1974,7 +1977,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
settings.providerDailyUsageBytes,
|
settings.providerDailyUsageBytes,
|
||||||
startedAt
|
startedAt
|
||||||
);
|
);
|
||||||
this.protectAgainstEmptyClobber = Boolean(options.protectEmptyClobber);
|
this.rollingAccountStatistics = new RollingAccountStatisticsAccumulator(this.statisticsLedger, startedAt);
|
||||||
|
this.protectAgainstEmptyClobber = Boolean(options.protectEmptyClobber);
|
||||||
if (this.protectAgainstEmptyClobber) {
|
if (this.protectAgainstEmptyClobber) {
|
||||||
logger.warn("Session-Schutz aktiv: Start mit unlesbarer Session — leere Speicherungen blockiert, bis echte Daten vorliegen");
|
logger.warn("Session-Schutz aktiv: Start mit unlesbarer Session — leere Speicherungen blockiert, bis echte Daten vorliegen");
|
||||||
}
|
}
|
||||||
@@ -2640,7 +2644,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
sessionRuntimeMs: this.getAppSessionRuntimeMs(now),
|
sessionRuntimeMs: this.getAppSessionRuntimeMs(now),
|
||||||
totalRuntimeMs: this.getLiveTotalRuntimeMs(now),
|
totalRuntimeMs: this.getLiveTotalRuntimeMs(now),
|
||||||
runtimeMeasuredAt: now,
|
runtimeMeasuredAt: now,
|
||||||
statistics: normalizeStatisticsLedger(this.statisticsLedger, now)
|
statistics: projectStatisticsLedger(this.statisticsLedger, now),
|
||||||
|
rolling24Hours: this.rollingAccountStatistics.snapshot(now)
|
||||||
};
|
};
|
||||||
this.statsCache = stats;
|
this.statsCache = stats;
|
||||||
this.statsCacheAt = now;
|
this.statsCacheAt = now;
|
||||||
@@ -2842,9 +2847,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
public resetDownloadStats(): void {
|
public resetDownloadStats(): void {
|
||||||
this.settings.totalDownloadedAllTime = 0;
|
this.settings.totalDownloadedAllTime = 0;
|
||||||
this.settings.totalCompletedFilesAllTime = 0;
|
this.settings.totalCompletedFilesAllTime = 0;
|
||||||
this.settings.providerTotalUsageBytes = {};
|
this.settings.providerTotalUsageBytes = {};
|
||||||
this.settings.debridLinkApiKeyTotalUsageBytes = {};
|
this.settings.debridLinkApiKeyTotalUsageBytes = {};
|
||||||
this.statisticsLedger = createStatisticsLedger();
|
this.statisticsLedger = createStatisticsLedger();
|
||||||
|
this.rollingAccountStatistics.reset(this.statisticsLedger);
|
||||||
this.statisticsDirty = false;
|
this.statisticsDirty = false;
|
||||||
this.statisticsUrgent = false;
|
this.statisticsUrgent = false;
|
||||||
saveStatisticsLedger(this.storagePaths.statisticsFile, this.statisticsLedger);
|
saveStatisticsLedger(this.storagePaths.statisticsFile, this.statisticsLedger);
|
||||||
@@ -8217,12 +8223,25 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private recordProviderDownloadedBytes(provider: DownloadItem["provider"], byteDelta: number, providerAccountId?: string): void {
|
private recordProviderDownloadedBytes(
|
||||||
if (!provider) {
|
provider: DownloadItem["provider"],
|
||||||
return;
|
byteDelta: number,
|
||||||
}
|
providerAccountId?: string,
|
||||||
|
providerAccountLabel?: string
|
||||||
|
): void {
|
||||||
|
if (!provider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const recordedAt = nowMs();
|
||||||
const effectiveProvider = resolveMegaDebridProvider(this.settings, provider) || provider;
|
const effectiveProvider = resolveMegaDebridProvider(this.settings, provider) || provider;
|
||||||
addStatisticsBytesInPlace(this.statisticsLedger, effectiveProvider, byteDelta);
|
addStatisticsBytesInPlace(this.statisticsLedger, effectiveProvider, byteDelta, recordedAt);
|
||||||
|
this.rollingAccountStatistics.record(
|
||||||
|
effectiveProvider,
|
||||||
|
byteDelta,
|
||||||
|
providerAccountId,
|
||||||
|
providerAccountLabel,
|
||||||
|
recordedAt
|
||||||
|
);
|
||||||
this.statisticsDirty = true;
|
this.statisticsDirty = true;
|
||||||
const nextUsage = addProviderDailyUsageBytes(this.settings, effectiveProvider, byteDelta);
|
const nextUsage = addProviderDailyUsageBytes(this.settings, effectiveProvider, byteDelta);
|
||||||
const nextTotalUsage = addProviderTotalUsageBytes(this.settings, effectiveProvider, byteDelta);
|
const nextTotalUsage = addProviderTotalUsageBytes(this.settings, effectiveProvider, byteDelta);
|
||||||
@@ -10851,7 +10870,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.session.totalDownloadedBytes += buffer.length;
|
this.session.totalDownloadedBytes += buffer.length;
|
||||||
this.sessionDownloadedBytes += buffer.length;
|
this.sessionDownloadedBytes += buffer.length;
|
||||||
this.settings.totalDownloadedAllTime += buffer.length;
|
this.settings.totalDownloadedAllTime += buffer.length;
|
||||||
this.recordProviderDownloadedBytes(item.provider, buffer.length, item.providerAccountId);
|
this.recordProviderDownloadedBytes(
|
||||||
|
item.provider,
|
||||||
|
buffer.length,
|
||||||
|
item.providerAccountId,
|
||||||
|
item.providerAccountLabel
|
||||||
|
);
|
||||||
this.itemContributedBytes.set(active.itemId, (this.itemContributedBytes.get(active.itemId) || 0) + buffer.length);
|
this.itemContributedBytes.set(active.itemId, (this.itemContributedBytes.get(active.itemId) || 0) + buffer.length);
|
||||||
this.recordSpeed(buffer.length, item.packageId);
|
this.recordSpeed(buffer.length, item.packageId);
|
||||||
throughputWindowBytes += buffer.length;
|
throughputWindowBytes += buffer.length;
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
|||||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||||
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
||||||
import { initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
import { initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
||||||
import { createStoragePaths, emptySession } from "../src/main/storage";
|
import { createStoragePaths, emptySession } from "../src/main/storage";
|
||||||
|
import { loadStatisticsLedger } from "../src/main/statistics-ledger";
|
||||||
import { getProviderRuntimeSnapshot, primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests, primeMegaDebridRuntimeCooldownForTests, resetMegaDebridRuntimeStateForTests, primeMegaDebridInFlightForTests, primeRealDebridRuntimeCooldownForTests, resetRealDebridRuntimeStateForTests } from "../src/main/debrid";
|
import { getProviderRuntimeSnapshot, primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests, primeMegaDebridRuntimeCooldownForTests, resetMegaDebridRuntimeStateForTests, primeMegaDebridInFlightForTests, primeRealDebridRuntimeCooldownForTests, resetRealDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||||
@@ -13352,6 +13353,68 @@ describe("download manager", () => {
|
|||||||
expect(internal.settings.realDebridAccountTotalUsageBytes).toEqual({ rda_one: 1000, rda_two: 2050 });
|
expect(internal.settings.realDebridAccountTotalUsageBytes).toEqual({ rda_one: 1000, rda_two: 2050 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("projects rolling traffic for each concrete account without exposing minute buckets", () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
providerDailyUsageDay: getProviderUsageDayKey(),
|
||||||
|
realDebridApiTokens: serializeRealDebridApiAccounts([
|
||||||
|
{ id: "rda_one", token: "token-one" },
|
||||||
|
{ id: "rda_two", token: "token-two" }
|
||||||
|
])
|
||||||
|
};
|
||||||
|
const manager = new DownloadManager(settings, emptySession(), createStoragePaths(path.join(root, "state")));
|
||||||
|
const internal = manager as unknown as {
|
||||||
|
recordProviderDownloadedBytes: (
|
||||||
|
provider: "realdebrid",
|
||||||
|
bytes: number,
|
||||||
|
providerAccountId?: string,
|
||||||
|
providerAccountLabel?: string
|
||||||
|
) => void;
|
||||||
|
statisticsLedger: { minutes: unknown[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
internal.recordProviderDownloadedBytes("realdebrid", 50, "rda_one", "Primary");
|
||||||
|
internal.recordProviderDownloadedBytes("realdebrid", 75, "rda_two", "Secondary");
|
||||||
|
const stats = manager.getStats();
|
||||||
|
|
||||||
|
expect(stats.rolling24Hours).toMatchObject({ downloadedBytes: 125 });
|
||||||
|
expect(stats.rolling24Hours?.accounts.map((account) => [account.id, account.label, account.bytes])).toEqual([
|
||||||
|
["rda_two", "Secondary", 75],
|
||||||
|
["rda_one", "Primary", 50]
|
||||||
|
]);
|
||||||
|
expect(stats.statistics?.minutes).toEqual([]);
|
||||||
|
expect(internal.statisticsLedger.minutes).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps rolling traffic on session reset and clears it on total reset", () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
providerDailyUsageDay: getProviderUsageDayKey(),
|
||||||
|
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_one", token: "token-one" }])
|
||||||
|
};
|
||||||
|
const manager = new DownloadManager(settings, emptySession(), createStoragePaths(path.join(root, "state")));
|
||||||
|
const internal = manager as unknown as {
|
||||||
|
recordProviderDownloadedBytes: (
|
||||||
|
provider: "realdebrid",
|
||||||
|
bytes: number,
|
||||||
|
providerAccountId?: string,
|
||||||
|
providerAccountLabel?: string
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
internal.recordProviderDownloadedBytes("realdebrid", 50, "rda_one", "Primary");
|
||||||
|
manager.resetSessionStats();
|
||||||
|
expect(manager.getStats().rolling24Hours?.downloadedBytes).toBe(50);
|
||||||
|
|
||||||
|
manager.resetDownloadStats();
|
||||||
|
expect(manager.getStats().rolling24Hours).toMatchObject({ downloadedBytes: 0, accounts: [] });
|
||||||
|
expect(loadStatisticsLedger(createStoragePaths(path.join(root, "state")).statisticsFile).minutes).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not recreate account usage when the source account was removed during the download", () => {
|
it("does not recreate account usage when the source account was removed during the download", () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
Reference in New Issue
Block a user