Skip to content

Commit d5add2b

Browse files
committed
[db] Introduce DBWorkspaceInstanceMetrics and persist all metrics from ws-manager-api into it
Tool: gitpod/catfood.gitpod.cloud
1 parent eafa98d commit d5add2b

File tree

6 files changed

+189
-1
lines changed

6 files changed

+189
-1
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright (c) 2025 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { Entity, Column, PrimaryColumn } from "typeorm";
8+
import { WorkspaceInstanceMetrics } from "@gitpod/gitpod-protocol";
9+
10+
@Entity()
11+
export class DBWorkspaceInstanceMetrics {
12+
@PrimaryColumn()
13+
instanceId: string;
14+
15+
@Column("json", { nullable: true })
16+
metrics?: WorkspaceInstanceMetrics;
17+
18+
@Column()
19+
_lastModified: Date;
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) 2025 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { MigrationInterface, QueryRunner } from "typeorm";
8+
9+
export class AddWorkspaceInstanceMetricsTable1739892121734 implements MigrationInterface {
10+
public async up(queryRunner: QueryRunner): Promise<any> {
11+
await queryRunner.query(`CREATE TABLE IF NOT EXISTS d_b_workspace_instance_metrics (
12+
instanceId char(36) NOT NULL,
13+
metrics JSON,
14+
_lastModified timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
15+
PRIMARY KEY (instanceId),
16+
KEY ind_dbsync (_lastModified)
17+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`);
18+
}
19+
20+
public async down(queryRunner: QueryRunner): Promise<any> {
21+
await queryRunner.query(`DROP TABLE IF EXISTS d_b_workspace_instance_metrics`);
22+
}
23+
}

components/gitpod-db/src/typeorm/workspace-db-impl.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
WorkspaceAndInstance,
2020
WorkspaceInfo,
2121
WorkspaceInstance,
22+
WorkspaceInstanceMetrics,
2223
WorkspaceInstanceUser,
2324
WorkspaceSession,
2425
WorkspaceType,
@@ -62,6 +63,7 @@ import { TypeORM } from "./typeorm";
6263
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
6364
import { DBProject } from "./entity/db-project";
6465
import { PrebuiltWorkspaceWithWorkspace } from "@gitpod/gitpod-protocol/src/protocol";
66+
import { DBWorkspaceInstanceMetrics } from "./entity/db-workspace-instance-metrics-db";
6567

6668
type RawTo<T> = (instance: WorkspaceInstance, ws: Workspace) => T;
6769
interface OrderBy {
@@ -109,6 +111,10 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
109111
);
110112
}
111113

114+
private async getWorkspaceInstanceMetricsRepo(): Promise<Repository<DBWorkspaceInstanceMetrics>> {
115+
return (await this.getEntityManager()).getRepository<DBWorkspaceInstanceMetrics>(DBWorkspaceInstanceMetrics);
116+
}
117+
112118
public async connect(maxTries: number = 3, timeout: number = 2000): Promise<void> {
113119
let tries = 1;
114120
while (tries <= maxTries) {
@@ -1143,6 +1149,36 @@ export class TypeORMWorkspaceDBImpl extends TransactionalDBImpl<WorkspaceDB> imp
11431149
const res = await query.getMany();
11441150
return res.map((r) => r.info);
11451151
}
1152+
1153+
async storeMetrics(instanceId: string, metrics: WorkspaceInstanceMetrics): Promise<WorkspaceInstanceMetrics> {
1154+
const repo = await this.getWorkspaceInstanceMetricsRepo();
1155+
const result = await repo.save({
1156+
instanceId,
1157+
metrics,
1158+
});
1159+
return result.metrics;
1160+
}
1161+
1162+
async getMetrics(instanceId: string): Promise<WorkspaceInstanceMetrics | undefined> {
1163+
const repo = await this.getWorkspaceInstanceMetricsRepo();
1164+
const dbMetrics = await repo.findOne({ where: { instanceId } });
1165+
return dbMetrics?.metrics;
1166+
}
1167+
1168+
async updateMetrics(
1169+
instanceId: string,
1170+
update: WorkspaceInstanceMetrics,
1171+
merge: (current: WorkspaceInstanceMetrics, update: WorkspaceInstanceMetrics) => WorkspaceInstanceMetrics,
1172+
): Promise<WorkspaceInstanceMetrics> {
1173+
return await this.transaction(async (db) => {
1174+
const current = await db.getMetrics(instanceId);
1175+
if (!current) {
1176+
return await db.storeMetrics(instanceId, update);
1177+
}
1178+
const merged = merge(current, update);
1179+
return await db.storeMetrics(instanceId, merged);
1180+
});
1181+
}
11461182
}
11471183

11481184
type InstanceJoinResult = DBWorkspace & { instance: WorkspaceInstance };

components/gitpod-db/src/workspace-db.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
WorkspaceSession,
2424
PrebuiltWorkspaceWithWorkspace,
2525
PrebuildWithStatus,
26+
WorkspaceInstanceMetrics,
2627
} from "@gitpod/gitpod-protocol";
2728

2829
export type MaybeWorkspace = Workspace | undefined;
@@ -196,4 +197,12 @@ export interface WorkspaceDB {
196197

197198
storePrebuildInfo(prebuildInfo: PrebuildInfo): Promise<void>;
198199
findPrebuildInfos(prebuildIds: string[]): Promise<PrebuildInfo[]>;
200+
201+
storeMetrics(instanceId: string, metrics: WorkspaceInstanceMetrics): Promise<WorkspaceInstanceMetrics>;
202+
getMetrics(instanceId: string): Promise<WorkspaceInstanceMetrics | undefined>;
203+
updateMetrics(
204+
instanceId: string,
205+
update: WorkspaceInstanceMetrics,
206+
merge: (current: WorkspaceInstanceMetrics, update: WorkspaceInstanceMetrics) => WorkspaceInstanceMetrics,
207+
): Promise<WorkspaceInstanceMetrics>;
199208
}

components/gitpod-protocol/src/workspace-instance.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,30 @@ export interface WorkspaceInstanceMetrics {
342342
*/
343343
workspaceImageSize: number;
344344
}>;
345+
346+
/**
347+
* Metrics about the workspace initializer
348+
*/
349+
initializerMetrics?: InitializerMetrics;
350+
}
351+
352+
export interface InitializerMetrics {
353+
git?: InitializerMetric;
354+
fileDownload?: InitializerMetric;
355+
snapshot?: InitializerMetric;
356+
backup?: InitializerMetric;
357+
prebuild?: InitializerMetric;
358+
composite?: InitializerMetric;
359+
}
360+
361+
export interface InitializerMetric {
362+
/**
363+
* Duration in milliseconds
364+
*/
365+
duration: number;
366+
367+
/**
368+
* Size in bytes
369+
*/
370+
size: number;
345371
}

components/ws-manager-bridge/src/bridge.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
PortProtocol as WsManPortProtocol,
2323
DescribeClusterRequest,
2424
WorkspaceType,
25+
InitializerMetrics,
26+
InitializerMetric,
2527
} from "@gitpod/ws-manager/lib";
2628
import { scrubber, TrustedValue } from "@gitpod/gitpod-protocol/lib/util/scrubbing";
2729
import { WorkspaceDB } from "@gitpod/gitpod-db/lib/workspace-db";
@@ -38,6 +40,7 @@ import { performance } from "perf_hooks";
3840
import { WorkspaceInstanceController } from "./workspace-instance-controller";
3941
import { PrebuildUpdater } from "./prebuild-updater";
4042
import { RedisPublisher } from "@gitpod/gitpod-db/lib";
43+
import { merge } from "ts-deepmerge";
4144

4245
export const WorkspaceManagerBridgeFactory = Symbol("WorkspaceManagerBridgeFactory");
4346

@@ -332,11 +335,13 @@ export class WorkspaceManagerBridge implements Disposable {
332335
instance.status.podName = instance.status.podName || status.runtime?.podName;
333336
instance.status.nodeIp = instance.status.nodeIp || status.runtime?.nodeIp;
334337
instance.status.ownerToken = status.auth!.ownerToken;
338+
// TODO(gpl): fade this our in favor of only using DBWorkspaceInstanceMetrics
335339
instance.status.metrics = {
336340
image: {
337341
totalSize: instance.status.metrics?.image?.totalSize || status.metadata.metrics?.image?.totalSize,
338342
workspaceImageSize:
339-
instance.status.metrics?.image?.totalSize || status.metadata.metrics?.image?.workspaceImageSize,
343+
instance.status.metrics?.image?.workspaceImageSize ||
344+
status.metadata.metrics?.image?.workspaceImageSize,
340345
},
341346
};
342347

@@ -411,6 +416,14 @@ export class WorkspaceManagerBridge implements Disposable {
411416
// now notify all prebuild listeners about updates - and update DB if needed
412417
await this.prebuildUpdater.updatePrebuiltWorkspace({ span }, userId, status);
413418

419+
// store metrics
420+
const instanceMetrics = mapInstanceMetrics(status);
421+
if (instanceMetrics) {
422+
await this.workspaceDB
423+
.trace(ctx)
424+
.updateMetrics(instance.id, instanceMetrics, mergeWorkspaceInstanceMetrics);
425+
}
426+
414427
// cleanup
415428
// important: call this after the DB update
416429
if (!!lifecycleHandler) {
@@ -501,3 +514,64 @@ function toWorkspaceType(type: WorkspaceType): protocol.WorkspaceType {
501514
}
502515
throw new Error("invalid WorkspaceType: " + type);
503516
}
517+
518+
function mergeWorkspaceInstanceMetrics(
519+
current: protocol.WorkspaceInstanceMetrics,
520+
update: protocol.WorkspaceInstanceMetrics,
521+
): protocol.WorkspaceInstanceMetrics {
522+
const merged = merge.withOptions({ mergeArrays: false, allowUndefinedOverrides: false }, current, update);
523+
return merged;
524+
}
525+
526+
function mapInstanceMetrics(status: WorkspaceStatus.AsObject): protocol.WorkspaceInstanceMetrics | undefined {
527+
let result: protocol.WorkspaceInstanceMetrics | undefined = undefined;
528+
529+
if (status.metadata?.metrics?.image) {
530+
result = result || {};
531+
result.image = {
532+
totalSize: status.metadata.metrics.image.totalSize,
533+
workspaceImageSize: status.metadata.metrics.image.workspaceImageSize,
534+
};
535+
}
536+
if (status.initializerMetrics) {
537+
result = result || {};
538+
result.initializerMetrics = mapInitializerMetrics(status.initializerMetrics);
539+
}
540+
541+
return result;
542+
}
543+
544+
function mapInitializerMetrics(metrics: InitializerMetrics.AsObject): protocol.InitializerMetrics {
545+
const result: protocol.InitializerMetrics = {};
546+
if (metrics.git) {
547+
result.git = mapInitializerMetric(metrics.git);
548+
}
549+
if (metrics.fileDownload) {
550+
result.fileDownload = mapInitializerMetric(metrics.fileDownload);
551+
}
552+
if (metrics.snapshot) {
553+
result.snapshot = mapInitializerMetric(metrics.snapshot);
554+
}
555+
if (metrics.backup) {
556+
result.backup = mapInitializerMetric(metrics.backup);
557+
}
558+
if (metrics.prebuild) {
559+
result.prebuild = mapInitializerMetric(metrics.prebuild);
560+
}
561+
if (metrics.composite) {
562+
result.composite = mapInitializerMetric(metrics.composite);
563+
}
564+
565+
return result;
566+
}
567+
568+
function mapInitializerMetric(metric: InitializerMetric.AsObject | undefined): protocol.InitializerMetric | undefined {
569+
if (!metric || !metric.duration) {
570+
return undefined;
571+
}
572+
573+
return {
574+
duration: metric.duration.seconds * 1000 + metric.duration.nanos / 1000000,
575+
size: metric.size,
576+
};
577+
}

0 commit comments

Comments
 (0)