Skip to content

Commit fd357fa

Browse files
committed
perf: optimize database & queries
Signed-off-by: BoxBoxJason <[email protected]>
1 parent 2470dcd commit fd357fa

File tree

10 files changed

+467
-186
lines changed

10 files changed

+467
-186
lines changed

src/database/controller/progressions.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ export namespace ProgressionController {
6262
{ name: criteriaName },
6363
increase
6464
);
65-
const updatedAchievements = await Progression.achieveCompletedAchievements(
66-
updatedProgressionsIds.map((progression) => progression.id)
67-
);
65+
const updatedAchievements =
66+
await Progression.achieveCompletedAchievements(
67+
updatedProgressionsIds.map((progression) => progression.id)
68+
);
6869
let awardedPoints = 0;
6970
for (let achievement of updatedAchievements) {
7071
// Notify the user of the unlocked achievement
@@ -98,9 +99,10 @@ export namespace ProgressionController {
9899
value.toString(),
99100
maximize
100101
);
101-
const updatedAchievements = await Progression.achieveCompletedAchievements(
102-
updatedProgressionsIds.map((progression) => progression.id)
103-
);
102+
const updatedAchievements =
103+
await Progression.achieveCompletedAchievements(
104+
updatedProgressionsIds.map((progression) => progression.id)
105+
);
104106
for (let achievement of updatedAchievements) {
105107
// Notify the user of the unlocked achievement
106108
awardAchievement(achievement);
@@ -109,4 +111,45 @@ export namespace ProgressionController {
109111
logger.error(`Failed to update progression: ${(error as Error).message}`);
110112
}
111113
}
114+
115+
/**
116+
* Update multiple progression values and check if any achievements have been unlocked
117+
*
118+
* @memberof achievements
119+
* @function updateProgressions
120+
*
121+
* @param {Array<{name: string, value: string | number | Date | boolean}>} updates - The list of progressions to update
122+
* @returns {Promise<void>}
123+
*/
124+
export async function updateProgressions(
125+
updates: Array<{
126+
name: string;
127+
value: string | number | Date | boolean;
128+
maximize?: boolean;
129+
}>
130+
): Promise<void> {
131+
try {
132+
const allUpdatedIds: number[] = [];
133+
for (const update of updates) {
134+
const updatedProgressionsIds = await Progression.updateValue(
135+
{ name: update.name },
136+
update.value.toString(),
137+
update.maximize
138+
);
139+
allUpdatedIds.push(...updatedProgressionsIds.map((p) => p.id));
140+
}
141+
142+
if (allUpdatedIds.length > 0) {
143+
const updatedAchievements =
144+
await Progression.achieveCompletedAchievements(allUpdatedIds);
145+
for (let achievement of updatedAchievements) {
146+
awardAchievement(achievement);
147+
}
148+
}
149+
} catch (error) {
150+
logger.error(
151+
`Failed to update progressions: ${(error as Error).message}`
152+
);
153+
}
154+
}
112155
}

src/database/controller/timespent.ts

Lines changed: 34 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,72 +17,53 @@ export namespace TimeSpentController {
1717
export async function updateTimeSpentFromSessions(): Promise<void> {
1818
const currentDate = new Date();
1919
const currentDateString = currentDate.toISOString().split("T")[0];
20-
// Update the daily counter
21-
logger.debug("JOB: Updating daily time spent");
22-
const dailyTimeSpent = await DailySession.calculateDuration(
23-
currentDateString,
24-
currentDateString
25-
);
26-
await ProgressionController.updateProgression(
27-
constants.criteria.DAILY_TIME_SPENT,
28-
dailyTimeSpent.toString()
29-
);
3020

31-
// Update the bi-monthly counter
32-
logger.debug("JOB: Updating bi-monthly time spent");
3321
const fourteenDaysAgo = new Date(
3422
currentDate.getTime() - 14 * 24 * 60 * 60 * 1000
3523
);
3624
const fourteenDaysAgoString = fourteenDaysAgo.toISOString().split("T")[0];
37-
const biMonthlyTimeSpent = await DailySession.calculateDuration(
38-
fourteenDaysAgoString,
39-
currentDateString
40-
);
41-
await ProgressionController.updateProgression(
42-
constants.criteria.TWO_WEEKS_TIME_SPENT,
43-
biMonthlyTimeSpent.toString()
44-
);
4525

46-
// Update the monthly counter
47-
logger.debug("JOB: Updating monthly time spent");
4826
const firstDayOfMonth = new Date(
4927
currentDate.getFullYear(),
5028
currentDate.getMonth(),
5129
1
5230
);
5331
const firstDayOfMonthString = firstDayOfMonth.toISOString().split("T")[0];
54-
const monthlyTimeSpent = await DailySession.calculateDuration(
55-
firstDayOfMonthString,
56-
currentDateString
57-
);
58-
await ProgressionController.updateProgression(
59-
constants.criteria.MONTHLY_TIME_SPENT,
60-
monthlyTimeSpent.toString()
61-
);
6232

63-
// Update the yearly counter
64-
logger.debug("JOB: Updating yearly time spent");
6533
const firstDayOfYear = new Date(currentDate.getFullYear(), 0, 1);
6634
const firstDayOfYearString = firstDayOfYear.toISOString().split("T")[0];
67-
const yearlyTimeSpent = await DailySession.calculateDuration(
68-
firstDayOfYearString,
69-
currentDateString
70-
);
71-
await ProgressionController.updateProgression(
72-
constants.criteria.YEARLY_TIME_SPENT,
73-
yearlyTimeSpent.toString()
74-
);
7535

76-
// Update the total counter
77-
logger.debug("JOB: Updating total time spent");
78-
const totalDuration = await DailySession.calculateDuration(
79-
"1970-01-01",
80-
currentDateString
81-
);
82-
await ProgressionController.updateProgression(
83-
constants.criteria.TOTAL_TIME_SPENT,
84-
totalDuration.toString()
85-
);
36+
logger.debug("JOB: Updating time spent counters");
37+
38+
const stats = await DailySession.getStatsSummary(
39+
currentDateString,
40+
fourteenDaysAgoString,
41+
firstDayOfMonthString,
42+
firstDayOfYearString
43+
);
44+
45+
await ProgressionController.updateProgressions([
46+
{
47+
name: constants.criteria.DAILY_TIME_SPENT,
48+
value: stats.daily.toString(),
49+
},
50+
{
51+
name: constants.criteria.TWO_WEEKS_TIME_SPENT,
52+
value: stats.twoWeeks.toString(),
53+
},
54+
{
55+
name: constants.criteria.MONTHLY_TIME_SPENT,
56+
value: stats.monthly.toString(),
57+
},
58+
{
59+
name: constants.criteria.YEARLY_TIME_SPENT,
60+
value: stats.yearly.toString(),
61+
},
62+
{
63+
name: constants.criteria.TOTAL_TIME_SPENT,
64+
value: stats.total.toString(),
65+
},
66+
]);
8667

8768
logger.debug("JOB: Time spent counters updated");
8869
}
@@ -140,7 +121,9 @@ export namespace TimeSpentController {
140121
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
141122
const yesterdayDateString = yesterdayDate.toISOString().split("T")[0];
142123

143-
const yesterdaySession = await DailySession.getOrCreate(yesterdayDateString);
124+
const yesterdaySession = await DailySession.getOrCreate(
125+
yesterdayDateString
126+
);
144127

145128
let newStreak = 1;
146129

src/database/model/migrations.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,22 +130,64 @@ export async function applyMigration(
130130
duration INTEGER NOT NULL
131131
)`
132132
);
133+
134+
const indexes_to_create: { [key: string]: string } = {
135+
// Index for achievement_criterias progression_id (Critical for checkAchievements)
136+
idx_achievement_criterias_progression_id:
137+
"achievement_criterias(progression_id)",
138+
// Index for achievements category filtering (UI)
139+
idx_achievements_category: "achievements(category)",
140+
// Index for achievements group filtering (UI)
141+
idx_achievements_group: 'achievements("group")',
142+
// Index for achievements achieved filtering (UI)
143+
idx_achievements_achieved: "achievements(achieved)",
144+
// Index for achievement_labels filtering (UI)
145+
idx_achievement_labels_label: "achievement_labels(label)",
146+
// Index for achievement_requirements requirement_id (Critical for achievable filter)
147+
idx_achievement_requirements_requirement_id:
148+
"achievement_requirements(requirement_id)",
149+
};
150+
151+
for (const [indexName, indexColumns] of Object.entries(
152+
indexes_to_create
153+
)) {
154+
db.run(
155+
`CREATE INDEX IF NOT EXISTS ${indexName} ON ${indexColumns}`
156+
);
157+
}
158+
133159
db.run("COMMIT");
134160
} catch (error) {
135161
db.run("ROLLBACK");
136162
throw error;
137163
}
138164
},
139165
down: () => {
166+
const tables_to_drop = [
167+
"schema_version",
168+
"achievements",
169+
"achievement_requirements",
170+
"progressions",
171+
"achievement_criterias",
172+
"achievement_labels",
173+
"daily_sessions",
174+
];
175+
const indexes_to_drop = [
176+
"idx_achievement_criterias_progression_id",
177+
"idx_achievements_category",
178+
"idx_achievements_group",
179+
"idx_achievements_achieved",
180+
"idx_achievement_labels_label",
181+
"idx_achievement_requirements_requirement_id",
182+
];
140183
db.run("BEGIN TRANSACTION");
141184
try {
142-
db.run("DROP TABLE IF EXISTS schema_version");
143-
db.run("DROP TABLE IF EXISTS achievements");
144-
db.run("DROP TABLE IF EXISTS achievement_requirements");
145-
db.run("DROP TABLE IF EXISTS progressions");
146-
db.run("DROP TABLE IF EXISTS achievement_criterias");
147-
db.run("DROP TABLE IF EXISTS achievement_labels");
148-
db.run("DROP TABLE IF EXISTS daily_sessions");
185+
for (const index of indexes_to_drop) {
186+
db.run(`DROP INDEX IF EXISTS ${index}`);
187+
}
188+
for (const table of tables_to_drop) {
189+
db.run(`DROP TABLE IF EXISTS ${table}`);
190+
}
149191
db.run("COMMIT");
150192
} catch (error) {
151193
db.run("ROLLBACK");

src/database/model/tables/Achievement.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,16 @@ class Achievement {
592592
conditions.push(`(${labelConditions.join(" AND ")})`);
593593
values.push(...filters.labels);
594594
}
595+
if (filters.criterias && filters.criterias.length > 0) {
596+
conditions.push(`EXISTS (
597+
SELECT 1 FROM achievement_criterias ac
598+
JOIN progressions p ON ac.progression_id = p.id
599+
WHERE ac.achievement_id = a.id AND p.name IN (${filters.criterias
600+
.map(() => "?")
601+
.join(", ")})
602+
)`);
603+
values.push(...filters.criterias);
604+
}
595605
if (filters.title) {
596606
conditions.push("a.title LIKE ?");
597607
values.push(`%${filters.title}%`);

src/database/model/tables/DailySession.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,41 @@ export class DailySession {
126126
);
127127
return res.total_duration as number;
128128
}
129+
130+
static async getStatsSummary(
131+
today: string,
132+
twoWeeksAgo: string,
133+
monthStart: string,
134+
yearStart: string
135+
): Promise<{
136+
daily: number;
137+
twoWeeks: number;
138+
monthly: number;
139+
yearly: number;
140+
total: number;
141+
}> {
142+
const db = await db_model.getDB();
143+
const query = `
144+
SELECT
145+
SUM(CASE WHEN date = ? THEN duration ELSE 0 END) as daily,
146+
SUM(CASE WHEN date >= ? THEN duration ELSE 0 END) as twoWeeks,
147+
SUM(CASE WHEN date >= ? THEN duration ELSE 0 END) as monthly,
148+
SUM(CASE WHEN date >= ? THEN duration ELSE 0 END) as yearly,
149+
SUM(duration) as total
150+
FROM daily_sessions
151+
`;
152+
const result = db_model.get(db, query, [
153+
today,
154+
twoWeeksAgo,
155+
monthStart,
156+
yearStart,
157+
]);
158+
return {
159+
daily: result.daily || 0,
160+
twoWeeks: result.twoWeeks || 0,
161+
monthly: result.monthly || 0,
162+
yearly: result.yearly || 0,
163+
total: result.total || 0,
164+
};
165+
}
129166
}

src/listeners/files.ts

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -154,47 +154,41 @@ export namespace fileListeners {
154154
path.extname(event.document.fileName)
155155
];
156156
if (language) {
157+
let totalLinesAdded = 0;
157158
for (const change of event.contentChanges) {
158-
await processContentChange(change, language);
159+
totalLinesAdded += countLinesAdded(change);
159160
}
160-
}
161-
}
162161

163-
async function processContentChange(
164-
change: vscode.TextDocumentContentChangeEvent,
165-
language: string
166-
): Promise<void> {
167-
// Check if the change involves adding new lines
168-
if (change.text.includes("\n")) {
169-
if (change.range.isSingleLine) {
170-
// Increment progression for the added line
162+
if (totalLinesAdded > 0) {
163+
// Increment progression for the added lines
171164
await ProgressionController.increaseProgression(
172-
constants.criteria.LINES_OF_CODE_LANGUAGE.replace("%s", language)
165+
constants.criteria.LINES_OF_CODE_LANGUAGE.replace("%s", language),
166+
totalLinesAdded
173167
);
174168
// Increment generic lines of code progression
175169
await ProgressionController.increaseProgression(
176-
constants.criteria.LINES_OF_CODE
170+
constants.criteria.LINES_OF_CODE,
171+
totalLinesAdded
177172
);
178-
} else {
179-
// Count non-empty lines added in the change
180-
const nonEmptyLinesCount = change.text
181-
.split(/\r?\n/)
182-
.filter((line) => line.trim().length > 0).length;
173+
}
174+
}
175+
}
183176

184-
// Increment progression for the added lines
185-
if (nonEmptyLinesCount > 0) {
186-
await ProgressionController.increaseProgression(
187-
constants.criteria.LINES_OF_CODE_LANGUAGE.replace("%s", language),
188-
nonEmptyLinesCount
189-
);
190-
// Increment generic lines of code progression
191-
await ProgressionController.increaseProgression(
192-
constants.criteria.LINES_OF_CODE,
193-
nonEmptyLinesCount
194-
);
195-
}
177+
function countLinesAdded(
178+
change: vscode.TextDocumentContentChangeEvent
179+
): number {
180+
// Check if the change involves adding new lines
181+
if (change.text.includes("\n")) {
182+
const nonEmptyLinesCount = change.text
183+
.split(/\r?\n/)
184+
.filter((line) => line.trim().length > 0).length;
185+
186+
if (nonEmptyLinesCount > 0) {
187+
return nonEmptyLinesCount;
196188
}
189+
return 1;
197190
}
191+
return 0;
198192
}
199193

200194
let fileErrorCounts = new Map<string, number>();

0 commit comments

Comments
 (0)