Skip to content

Commit 058a4e1

Browse files
insignmrm007ampcode-com
authored
Port PR NoeFabris#460: reduce excessive account state writes (#8)
* fix: reduce excessive file writes from account state updates - Remove unconditional requestSaveToDisk() on every API request (line 1702) - Add snapshot deduplication to skip no-op writes - Increase debounce from 1s to 5s for rate-limit storm resilience - Move save trigger to after markAccountUsed() where lastUsed is persisted Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019c6dae-36aa-7524-849b-d8c515ba0cd1 (cherry picked from commit a4ac1e4) * address review: shallow-copy rateLimitResetTimes in buildStorageState, add test for state-change write Amp-Thread-ID: https://ampcode.com/threads/T-019c6dae-36aa-7524-849b-d8c515ba0cd1 Co-authored-by: Amp <amp@ampcode.com> (cherry picked from commit f7d1145) --------- Co-authored-by: Remus Mate <mrm_dev@outlook.com> Co-authored-by: Amp <amp@ampcode.com>
1 parent 913a814 commit 058a4e1

File tree

3 files changed

+89
-30
lines changed

3 files changed

+89
-30
lines changed

src/plugin.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1706,8 +1706,6 @@ export const createAntigravityPlugin = (providerId: string) => async (
17061706
accountManager.markToastShown(account.index);
17071707
}
17081708

1709-
accountManager.requestSaveToDisk();
1710-
17111709
let authRecord = accountManager.toAuthDetails(account);
17121710

17131711
if (accessTokenExpired(authRecord)) {
@@ -2296,7 +2294,8 @@ export const createAntigravityPlugin = (providerId: string) => async (
22962294
account.consecutiveFailures = 0;
22972295
getHealthTracker().recordSuccess(account.index);
22982296
accountManager.markAccountUsed(account.index);
2299-
2297+
accountManager.requestSaveToDisk();
2298+
23002299
void triggerAsyncQuotaRefreshForAccount(
23012300
accountManager,
23022301
account.index,

src/plugin/accounts.test.ts

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22

33
import { AccountManager, type ModelFamily, type HeaderStyle, parseRateLimitReason, calculateBackoffMs, type RateLimitReason, resolveQuotaGroup } from "./accounts";
4-
import type { AccountStorageV4 } from "./storage";
4+
import { saveAccounts, type AccountStorageV4 } from "./storage";
55
import type { OAuthAuthDetails } from "./types";
66

77
// Mock storage to prevent test data from leaking to real config files
@@ -1069,6 +1069,8 @@ describe("AccountManager", () => {
10691069
describe("Issue #174: saveToDisk throttling", () => {
10701070
it("requestSaveToDisk coalesces multiple calls into one write", async () => {
10711071
vi.useFakeTimers();
1072+
const saveMock = vi.mocked(saveAccounts);
1073+
saveMock.mockClear();
10721074

10731075
const stored: AccountStorageV4 = {
10741076
version: 4,
@@ -1079,23 +1081,22 @@ describe("AccountManager", () => {
10791081
};
10801082

10811083
const manager = new AccountManager(undefined, stored);
1082-
const saveSpy = vi.spyOn(manager, "saveToDisk").mockResolvedValue();
10831084

10841085
manager.requestSaveToDisk();
10851086
manager.requestSaveToDisk();
10861087
manager.requestSaveToDisk();
10871088

1088-
expect(saveSpy).not.toHaveBeenCalled();
1089+
expect(saveMock).not.toHaveBeenCalled();
10891090

1090-
await vi.advanceTimersByTimeAsync(1500);
1091+
await vi.advanceTimersByTimeAsync(5500);
10911092

1092-
expect(saveSpy).toHaveBeenCalledTimes(1);
1093-
1094-
saveSpy.mockRestore();
1093+
expect(saveMock).toHaveBeenCalledTimes(1);
10951094
});
10961095

10971096
it("flushSaveToDisk waits for pending save to complete", async () => {
10981097
vi.useFakeTimers();
1098+
const saveMock = vi.mocked(saveAccounts);
1099+
saveMock.mockClear();
10991100

11001101
const stored: AccountStorageV4 = {
11011102
version: 4,
@@ -1106,22 +1107,21 @@ describe("AccountManager", () => {
11061107
};
11071108

11081109
const manager = new AccountManager(undefined, stored);
1109-
const saveSpy = vi.spyOn(manager, "saveToDisk").mockResolvedValue();
11101110

11111111
manager.requestSaveToDisk();
11121112

11131113
const flushPromise = manager.flushSaveToDisk();
11141114

1115-
await vi.advanceTimersByTimeAsync(1500);
1115+
await vi.advanceTimersByTimeAsync(5500);
11161116
await flushPromise;
11171117

1118-
expect(saveSpy).toHaveBeenCalledTimes(1);
1119-
1120-
saveSpy.mockRestore();
1118+
expect(saveMock).toHaveBeenCalledTimes(1);
11211119
});
11221120

11231121
it("does not save again if no new requestSaveToDisk after flush", async () => {
11241122
vi.useFakeTimers();
1123+
const saveMock = vi.mocked(saveAccounts);
1124+
saveMock.mockClear();
11251125

11261126
const stored: AccountStorageV4 = {
11271127
version: 4,
@@ -1132,18 +1132,68 @@ describe("AccountManager", () => {
11321132
};
11331133

11341134
const manager = new AccountManager(undefined, stored);
1135-
const saveSpy = vi.spyOn(manager, "saveToDisk").mockResolvedValue();
11361135

11371136
manager.requestSaveToDisk();
1138-
await vi.advanceTimersByTimeAsync(1500);
1137+
await vi.advanceTimersByTimeAsync(5500);
1138+
1139+
expect(saveMock).toHaveBeenCalledTimes(1);
11391140

1140-
expect(saveSpy).toHaveBeenCalledTimes(1);
1141+
await vi.advanceTimersByTimeAsync(6000);
1142+
1143+
expect(saveMock).toHaveBeenCalledTimes(1);
1144+
});
1145+
1146+
it("writes when account state changes between debounced saves", async () => {
1147+
vi.useFakeTimers();
1148+
const saveMock = vi.mocked(saveAccounts);
1149+
saveMock.mockClear();
1150+
1151+
const stored: AccountStorageV4 = {
1152+
version: 4,
1153+
accounts: [
1154+
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
1155+
],
1156+
activeIndex: 0,
1157+
};
11411158

1142-
await vi.advanceTimersByTimeAsync(3000);
1159+
const manager = new AccountManager(undefined, stored);
11431160

1144-
expect(saveSpy).toHaveBeenCalledTimes(1);
1161+
// First save
1162+
manager.requestSaveToDisk();
1163+
await vi.advanceTimersByTimeAsync(5500);
1164+
expect(saveMock).toHaveBeenCalledTimes(1);
11451165

1146-
saveSpy.mockRestore();
1166+
// Mutate state, then request save — should write again
1167+
manager.markAccountUsed(0);
1168+
manager.requestSaveToDisk();
1169+
await vi.advanceTimersByTimeAsync(5500);
1170+
expect(saveMock).toHaveBeenCalledTimes(2);
1171+
});
1172+
1173+
it("skips write when account state has not changed", async () => {
1174+
vi.useFakeTimers();
1175+
const saveMock = vi.mocked(saveAccounts);
1176+
saveMock.mockClear();
1177+
1178+
const stored: AccountStorageV4 = {
1179+
version: 4,
1180+
accounts: [
1181+
{ refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 },
1182+
],
1183+
activeIndex: 0,
1184+
};
1185+
1186+
const manager = new AccountManager(undefined, stored);
1187+
1188+
// First save — should write
1189+
manager.requestSaveToDisk();
1190+
await vi.advanceTimersByTimeAsync(5500);
1191+
expect(saveMock).toHaveBeenCalledTimes(1);
1192+
1193+
// Second save with no state change — should skip
1194+
manager.requestSaveToDisk();
1195+
await vi.advanceTimersByTimeAsync(5500);
1196+
expect(saveMock).toHaveBeenCalledTimes(1);
11471197
});
11481198
});
11491199

src/plugin/accounts.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ export class AccountManager {
312312
private savePending = false;
313313
private saveTimeout: ReturnType<typeof setTimeout> | null = null;
314314
private savePromiseResolvers: Array<() => void> = [];
315+
private lastSavedSnapshot: string | null = null;
315316

316317
static async loadFromDisk(authFallback?: OAuthAuthDetails): Promise<AccountManager> {
317318
const stored = await loadAccounts();
@@ -991,11 +992,11 @@ export class AccountManager {
991992
return [...this.accounts];
992993
}
993994

994-
async saveToDisk(): Promise<void> {
995+
private buildStorageState(): AccountStorageV4 {
995996
const claudeIndex = Math.max(0, this.currentAccountIndexByFamily.claude);
996997
const geminiIndex = Math.max(0, this.currentAccountIndexByFamily.gemini);
997-
998-
const storage: AccountStorageV4 = {
998+
999+
return {
9991000
version: 4,
10001001
accounts: this.accounts.map((a) => ({
10011002
email: a.email,
@@ -1006,7 +1007,7 @@ export class AccountManager {
10061007
lastUsed: a.lastUsed,
10071008
enabled: a.enabled,
10081009
lastSwitchReason: a.lastSwitchReason,
1009-
rateLimitResetTimes: Object.keys(a.rateLimitResetTimes).length > 0 ? a.rateLimitResetTimes : undefined,
1010+
rateLimitResetTimes: Object.keys(a.rateLimitResetTimes).length > 0 ? { ...a.rateLimitResetTimes } : undefined,
10101011
coolingDownUntil: a.coolingDownUntil,
10111012
cooldownReason: a.cooldownReason,
10121013
fingerprint: a.fingerprint,
@@ -1023,9 +1024,13 @@ export class AccountManager {
10231024
claude: claudeIndex,
10241025
gemini: geminiIndex,
10251026
},
1026-
};
1027+
}
1028+
}
10271029

1028-
await saveAccounts(storage);
1030+
async saveToDisk(): Promise<void> {
1031+
const state = this.buildStorageState();
1032+
await saveAccounts(state);
1033+
this.lastSavedSnapshot = JSON.stringify(state);
10291034
}
10301035

10311036
requestSaveToDisk(): void {
@@ -1035,7 +1040,7 @@ export class AccountManager {
10351040
this.savePending = true;
10361041
this.saveTimeout = setTimeout(() => {
10371042
void this.executeSave();
1038-
}, 1000);
1043+
}, 5000);
10391044
}
10401045

10411046
async flushSaveToDisk(): Promise<void> {
@@ -1050,9 +1055,14 @@ export class AccountManager {
10501055
private async executeSave(): Promise<void> {
10511056
this.savePending = false;
10521057
this.saveTimeout = null;
1053-
1058+
10541059
try {
1055-
await this.saveToDisk();
1060+
const state = this.buildStorageState();
1061+
const snapshot = JSON.stringify(state);
1062+
if (snapshot !== this.lastSavedSnapshot) {
1063+
await saveAccounts(state);
1064+
this.lastSavedSnapshot = snapshot;
1065+
}
10561066
} catch {
10571067
// best-effort persistence; avoid unhandled rejection from timer-driven saves
10581068
} finally {

0 commit comments

Comments
 (0)