Skip to content

Commit 90c1810

Browse files
committed
Buffer heartbeats in memory and send once every 30 seconds to api
1 parent 475b669 commit 90c1810

File tree

3 files changed

+132
-53
lines changed

3 files changed

+132
-53
lines changed

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export enum LogLevel {
1717

1818
export const AI_RECENT_PASTES_TIME_MS = 500;
1919
export const TIME_BETWEEN_HEARTBEATS_MS = 120000;
20+
export const SEND_BUFFER_SECONDS = 30;

src/desktop.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as fs from 'fs';
22
import * as os from 'os';
3+
import * as child_process from 'child_process';
4+
import { StdioOptions } from 'child_process';
35

46
export class Desktop {
57
public static isWindows(): boolean {
@@ -12,17 +14,18 @@ export class Desktop {
1214

1315
public static getHomeDirectory(): string {
1416
let home = process.env.WAKATIME_HOME;
15-
if (home && home.trim() && fs.existsSync(home.trim()))
16-
return home.trim();
17-
if (this.isPortable())
18-
return process.env['VSCODE_PORTABLE'] as string;
17+
if (home && home.trim() && fs.existsSync(home.trim())) return home.trim();
18+
if (this.isPortable()) return process.env['VSCODE_PORTABLE'] as string;
1919
return process.env[this.isWindows() ? 'USERPROFILE' : 'HOME'] || process.cwd();
2020
}
2121

22-
public static buildOptions(): Object {
23-
const options = {
22+
public static buildOptions(stdin?: boolean): Object {
23+
const options: child_process.ExecFileOptions = {
2424
windowsHide: true,
2525
};
26+
if (stdin) {
27+
(options as any).stdio = ['pipe', 'pipe', 'pipe'] as StdioOptions;
28+
}
2629
if (!this.isWindows() && !process.env.WAKATIME_HOME && !process.env.HOME) {
2730
options['env'] = { ...process.env, WAKATIME_HOME: this.getHomeDirectory() };
2831
}

src/wakatime.ts

Lines changed: 122 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import * as fs from 'fs';
44
import * as path from 'path';
55
import * as vscode from 'vscode';
66

7-
import { AI_RECENT_PASTES_TIME_MS, COMMAND_DASHBOARD, LogLevel } from './constants';
7+
import {
8+
AI_RECENT_PASTES_TIME_MS,
9+
COMMAND_DASHBOARD,
10+
LogLevel,
11+
SEND_BUFFER_SECONDS,
12+
} from './constants';
813
import { Options, Setting } from './options';
914

1015
import { Dependencies } from './dependencies';
@@ -21,6 +26,19 @@ interface FileSelectionMap {
2126
[key: string]: FileSelection;
2227
}
2328

29+
interface Heartbeat {
30+
time: number;
31+
entity: string;
32+
is_write: boolean;
33+
lineno: number;
34+
cursorpos: number;
35+
lines_in_file: number;
36+
alternate_project?: string;
37+
project_folder?: string;
38+
category?: 'debugging' | 'ai coding' | 'building' | 'code reviewing';
39+
is_unsaved_entity?: boolean;
40+
}
41+
2442
export class WakaTime {
2543
private agentName: string;
2644
private extension: any;
@@ -59,6 +77,8 @@ export class WakaTime {
5977
private resourcesLocation: string;
6078
private lastApiKeyPrompted: number = 0;
6179
private isMetricsEnabled: boolean = false;
80+
private heartbeats: Heartbeat[] = [];
81+
private lastSent: number = 0;
6282

6383
constructor(extensionPath: string, logger: Logger) {
6484
this.extensionPath = extensionPath;
@@ -97,6 +117,7 @@ export class WakaTime {
97117
}
98118

99119
public dispose() {
120+
this.sendHeartbeats();
100121
this.statusBar?.dispose();
101122
this.statusBarTeamYou?.dispose();
102123
this.statusBarTeamOther?.dispose();
@@ -520,6 +541,10 @@ export class WakaTime {
520541
}
521542

522543
private onEvent(isWrite: boolean): void {
544+
if (Date.now() - this.lastSent > SEND_BUFFER_SECONDS * 1000) {
545+
this.sendHeartbeats();
546+
}
547+
523548
clearTimeout(this.debounceTimeoutId);
524549
this.debounceTimeoutId = setTimeout(() => {
525550
if (this.disabled) return;
@@ -543,7 +568,7 @@ export class WakaTime {
543568
this.lastCompile !== this.isCompiling ||
544569
this.lastAICodeGenerating !== this.isAICodeGenerating
545570
) {
546-
this.sendHeartbeat(
571+
this.appendHeartbeat(
547572
doc,
548573
time,
549574
editor.selection.start,
@@ -564,32 +589,7 @@ export class WakaTime {
564589
}, this.debounceMs);
565590
}
566591

567-
private async sendHeartbeat(
568-
doc: vscode.TextDocument,
569-
time: number,
570-
selection: vscode.Position,
571-
isWrite: boolean,
572-
isCompiling: boolean,
573-
isDebugging: boolean,
574-
isAICoding: boolean,
575-
): Promise<void> {
576-
const apiKey = await this.options.getApiKey();
577-
if (apiKey) {
578-
await this._sendHeartbeat(
579-
doc,
580-
time,
581-
selection,
582-
isWrite,
583-
isCompiling,
584-
isDebugging,
585-
isAICoding,
586-
);
587-
} else {
588-
await this.promptForApiKey();
589-
}
590-
}
591-
592-
private async _sendHeartbeat(
592+
private async appendHeartbeat(
593593
doc: vscode.TextDocument,
594594
time: number,
595595
selection: vscode.Position,
@@ -610,25 +610,75 @@ export class WakaTime {
610610
// prevent sending the same heartbeat (https://github.com/wakatime/vscode-wakatime/issues/163)
611611
if (isWrite && this.isDuplicateHeartbeat(file, time, selection)) return;
612612

613+
const now = Date.now();
614+
615+
const heartbeat: Heartbeat = {
616+
entity: file,
617+
time: now / 1000,
618+
is_write: isWrite,
619+
lineno: selection.line + 1,
620+
cursorpos: selection.character + 1,
621+
lines_in_file: doc.lineCount,
622+
};
623+
624+
if (isDebugging) {
625+
heartbeat.category = 'debugging';
626+
} else if (isCompiling) {
627+
heartbeat.category = 'building';
628+
} else if (isAICoding) {
629+
heartbeat.category = 'ai coding';
630+
} else if (Utils.isPullRequest(doc.uri)) {
631+
heartbeat.category = 'code reviewing';
632+
}
633+
634+
const project = this.getProjectName(doc.uri);
635+
if (project) heartbeat.alternate_project = project;
636+
637+
const folder = this.getProjectFolder(doc.uri);
638+
if (folder) heartbeat.project_folder = folder;
639+
640+
if (doc.isUntitled) heartbeat.is_unsaved_entity = true;
641+
642+
this.logger.debug(`Appending heartbeat to local buffer: ${JSON.stringify(heartbeat, null, 2)}`);
643+
this.heartbeats.push(heartbeat);
644+
645+
if (now - this.lastSent > SEND_BUFFER_SECONDS * 1000) {
646+
await this.sendHeartbeats();
647+
}
648+
}
649+
650+
private async sendHeartbeats(): Promise<void> {
651+
const apiKey = await this.options.getApiKey();
652+
if (apiKey) {
653+
await this._sendHeartbeats();
654+
} else {
655+
await this.promptForApiKey();
656+
}
657+
}
658+
659+
private async _sendHeartbeats(): Promise<void> {
660+
if (!this.dependencies.isCliInstalled()) return;
661+
662+
const heartbeat = this.heartbeats.shift();
663+
if (!heartbeat) return;
664+
665+
this.lastSent = Date.now();
666+
613667
let args: string[] = [];
614668

615-
args.push('--entity', Utils.quote(file));
669+
args.push('--entity', Utils.quote(heartbeat.entity));
670+
671+
args.push('--time', String(heartbeat.time));
616672

617673
let user_agent =
618674
this.agentName + '/' + vscode.version + ' vscode-wakatime/' + this.extension.version;
619675
args.push('--plugin', Utils.quote(user_agent));
620676

621-
args.push('--lineno', String(selection.line + 1));
622-
args.push('--cursorpos', String(selection.character + 1));
623-
args.push('--lines-in-file', String(doc.lineCount));
624-
if (isDebugging) {
625-
args.push('--category', 'debugging');
626-
} else if (isCompiling) {
627-
args.push('--category', 'building');
628-
} else if (isAICoding) {
629-
args.push('--category', 'ai coding');
630-
} else if (Utils.isPullRequest(doc.uri)) {
631-
args.push('--category', 'code reviewing');
677+
args.push('--lineno', String(heartbeat.lineno));
678+
args.push('--cursorpos', String(heartbeat.cursorpos));
679+
args.push('--lines-in-file', String(heartbeat.lines_in_file));
680+
if (heartbeat.category) {
681+
args.push('--category', heartbeat.category);
632682
}
633683

634684
if (this.isMetricsEnabled) args.push('--metrics');
@@ -639,13 +689,15 @@ export class WakaTime {
639689
const apiUrl = await this.options.getApiUrl();
640690
if (apiUrl) args.push('--api-url', Utils.quote(apiUrl));
641691

642-
const project = this.getProjectName(doc.uri);
643-
if (project) args.push('--alternate-project', Utils.quote(project));
692+
if (heartbeat.alternate_project) {
693+
args.push('--alternate-project', Utils.quote(heartbeat.alternate_project));
694+
}
644695

645-
const folder = this.getProjectFolder(doc.uri);
646-
if (folder) args.push('--project-folder', Utils.quote(folder));
696+
if (heartbeat.project_folder) {
697+
args.push('--project-folder', Utils.quote(heartbeat.project_folder));
698+
}
647699

648-
if (isWrite) args.push('--write');
700+
if (heartbeat.is_write) args.push('--write');
649701

650702
if (Desktop.isWindows() || Desktop.isPortable()) {
651703
args.push(
@@ -656,18 +708,32 @@ export class WakaTime {
656708
);
657709
}
658710

659-
if (doc.isUntitled) args.push('--is-unsaved-entity');
711+
if (heartbeat.is_unsaved_entity) args.push('--is-unsaved-entity');
712+
713+
const extraHeartbeats = this.getExtraHeartbeats();
714+
if (extraHeartbeats.length > 0) args.push('--extra-heartbeats');
660715

661716
const binary = this.dependencies.getCliLocation();
662717
this.logger.debug(`Sending heartbeat: ${Utils.formatArguments(binary, args)}`);
663-
const options = Desktop.buildOptions();
718+
const options = Desktop.buildOptions(extraHeartbeats.length > 0);
664719
let proc = child_process.execFile(binary, args, options, (error, stdout, stderr) => {
665720
if (error != null) {
666721
if (stderr && stderr.toString() != '') this.logger.error(stderr.toString());
667722
if (stdout && stdout.toString() != '') this.logger.error(stdout.toString());
668723
this.logger.error(error.toString());
669724
}
670725
});
726+
727+
// send any extra heartbeats
728+
if (proc.stdin) {
729+
proc.stdin.write(JSON.stringify(extraHeartbeats));
730+
proc.stdin.write('\n');
731+
proc.stdin.end();
732+
} else if (extraHeartbeats.length > 0) {
733+
this.logger.error('Unable to set stdio[0] to pipe');
734+
this.heartbeats.push(...extraHeartbeats);
735+
}
736+
671737
proc.on('close', async (code, _signal) => {
672738
if (code == 0) {
673739
if (this.showStatusBar) this.getCodingActivity();
@@ -712,6 +778,15 @@ export class WakaTime {
712778
});
713779
}
714780

781+
private getExtraHeartbeats() {
782+
const heartbeats: Heartbeat[] = [];
783+
while (true) {
784+
const h = this.heartbeats.shift();
785+
if (!h) return heartbeats;
786+
heartbeats.push(h);
787+
}
788+
}
789+
715790
private async getCodingActivity() {
716791
if (!this.showStatusBar) return;
717792

0 commit comments

Comments
 (0)