Skip to content

Commit

Permalink
feat(sdk): resource usage report (#5243)
Browse files Browse the repository at this point in the history
## Description
closes #4644 
As a part of the matrix automation process, we need to figure out what are the methods and properties that are used in each test. I came out with the following approach:
- collect the inflight methods from the `_addOnLift`
- collect the preflight methods and properties by adding a proxy to the Resource class
- save the collected methods list to a file in the output dir (currently, it may change in the future)

### Implemented by adding a new platform (@hasanaburayyan FYI)

Please let me know what you think :) 

TODO:
- [x] Add tests
- [ ] ~Change platform documentation (to reflect the change in the newInstance method)~
- [ ] Prefix "internal" prelight properties and methods with a "_"
 
## Checklist

- [ ] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted)
- [ ] Description explains motivation and solution
- [ ] Tests added (always)
- [ ] Docs updated (only required for features)
- [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
tsuf239 authored Jan 18, 2024
1 parent 032688d commit 0d19f9e
Show file tree
Hide file tree
Showing 22 changed files with 453 additions and 70 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,8 +243,8 @@ jobs:

- name: Install dependencies
run: |
pnpm install --frozen-lockfile --ignore-scripts --filter hangar --filter examples-valid --filter examples-invalid
pnpm run compile --filter jsii-fixture
pnpm install --frozen-lockfile --ignore-scripts --filter hangar --filter examples-valid --filter examples-invalid --filter @winglang/compatibility-spy
pnpm run compile --filter jsii-fixture --filter @winglang/compatibility-spy
- name: Run Hangar Tests
working-directory: tools/hangar
Expand Down Expand Up @@ -385,7 +385,7 @@ jobs:
set -o pipefail
cd dist
PACKAGES=("@winglang/sdk" "ts4w" "@winglang/compiler" "@wingconsole/design-system" "@wingconsole/ui" "@wingconsole/server" "@wingconsole/app" "winglang" "@winglang/platform-awscdk")
PACKAGES=("@winglang/sdk" "ts4w" "@winglang/compiler" "@wingconsole/design-system" "@wingconsole/ui" "@wingconsole/server" "@wingconsole/app" "winglang" "@winglang/platform-awscdk" "@winglang/compatibility-spy")
for PACKAGE in "${PACKAGES[@]}"; do
# Check if already published
VERSION_FOUND=$(npm view "$PACKAGE@$PACKAGE_VERSION" version --verbose || true)
Expand Down
3 changes: 3 additions & 0 deletions libs/awscdk/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"overrides": []
}
7 changes: 2 additions & 5 deletions libs/awscdk/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export class App extends core.App {

private synthed: boolean;
private synthedOutput: string | undefined;
private synthHooks?: core.SynthHooks;

constructor(props: CdkAppProps) {
let stackName = props.stackName ?? process.env.CDK_STACK_NAME;
Expand Down Expand Up @@ -102,8 +101,6 @@ export class App extends core.App {
...args: any[]
) => this.newAbstract(fqn, scope, id, ...args);

this.synthHooks = props.synthHooks;

this.outdir = outdir;
this.cdkApp = cdkApp;
this.cdkStack = cdkStack;
Expand All @@ -128,8 +125,8 @@ export class App extends core.App {
// call preSynthesize() on every construct in the tree
core.preSynthesizeAllConstructs(this);

if (this.synthHooks?.preSynthesize) {
this.synthHooks.preSynthesize.forEach((hook) => hook(this));
if (this._synthHooks?.preSynthesize) {
this._synthHooks.preSynthesize.forEach((hook) => hook(this));
}

// synthesize cdk.Stack files in `outdir/cdk.out`
Expand Down
1 change: 1 addition & 0 deletions libs/compatibility-spy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib/
3 changes: 3 additions & 0 deletions libs/compatibility-spy/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"overrides": []
}
43 changes: 43 additions & 0 deletions libs/compatibility-spy/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@winglang/compatibility-spy",
"version": "0.0.0",
"author": {
"name": "Wing Cloud",
"email": "[email protected]",
"organization": true
},
"repository": {
"type": "git",
"url": "https://github.com/winglang/wing.git",
"directory": "libs/awscdk"
},
"description": "Wing Platform for spying the usage of cloud resources' methods and properties",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org",
"tag": "latest"
},
"main": "index.js",
"license": "MIT",
"dependencies": {
"constructs": "~10.2.69",
"@winglang/sdk": "workspace:^"
},
"devDependencies": {
"bump-pack": "workspace:^",
"typescript": "^4.9.5",
"tsup": "^6.7.0",
"vitest": "^0.32.4"
},
"scripts": {
"compile": "tsc",
"package": "bump-pack -b",
"test": "vitest run --passWithNoTests --update"
},
"files": [
"lib"
],
"volta": {
"extends": "../../package.json"
}
}
98 changes: 98 additions & 0 deletions libs/compatibility-spy/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { writeFileSync } from "fs";
import { resolve } from "path";
import { IPlatform } from "@winglang/sdk/lib/platform";
import { Construct } from "constructs";
import { App } from "@winglang/sdk/lib/core";

const PARENT_PROPERTIES: Set<string> = new Set([
"node",
"onLiftMap",
...Object.getOwnPropertyNames(Construct),
...Object.getOwnPropertyNames(Construct.prototype),
]);

export class Platform implements IPlatform {
public readonly target = "*";
/**
* A summary of methods and property usage for each resource time
* @internal
*/
public _usageContext: Map<string, Set<string>> = new Map();

newInstance(fqn: string, scope: Construct, id: string, ...args: any) {
//@ts-expect-error - accessing protected method
const type = App.of(scope).typeForFqn(fqn);

if (!type) {
return undefined;
}

return new Proxy(new type(scope, id, ...args), {
get: (target, prop: string | Symbol) => {
if (
typeof prop === "string" &&
!prop.startsWith("_") &&
!PARENT_PROPERTIES.has(prop)
) {
this._addToUsageContext(target, prop);
}
//@ts-ignore
return target[prop];
},
set: (target, prop, newValue) => {
if (typeof prop === "string" && !prop.startsWith("_")) {
this._addToUsageContext(target, prop);
}
//@ts-ignore
target[prop] = newValue;
return true;
},
});
}

preSynth(app: Construct) {
for (const c of app.node.findAll()) {
if ((c as any).onLiftMap?.size) {
(c as any).onLiftMap.forEach((ops: Set<string>) => {
ops.forEach((op: string) => this._addToUsageContext(c, op));
});
}
}
this._writeAppUsage(app as App);
}

/**
* Adds an op to usage context
* @param op
* @internal
*/
public _addToUsageContext(parent: any, op: string): void {
const className = parent.constructor.name;

if (["handle", "$inflight_init"].includes(op)) {
return;
}
const usageContext = this._usageContext.get(className);
if (!usageContext) {
this._usageContext.set(className, new Set([op]));
} else {
usageContext.add(op);
}
}

/**
* Write the usage context into a file in the out dir
* @internal
*/
private _writeAppUsage(app: App): void {
const context: Record<string, string[]> = {};
for (const key of this._usageContext.keys()) {
context[key] = Array.from(this._usageContext.get(key) ?? []);
}

writeFileSync(
resolve(app.outdir, "usage_context.json"),
JSON.stringify(context, null, 2)
);
}
}
56 changes: 56 additions & 0 deletions libs/compatibility-spy/test/spy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { test, describe, expect, vi } from "vitest";
import { join } from "path";
import {
PlatformManager,
_loadCustomPlatform,
} from "@winglang/sdk/lib/platform";
import { Platform } from "../src";
import { BUCKET_FQN } from "@winglang/sdk/lib/cloud";
import { App as SimApp } from "@winglang/sdk/lib/target-sim/app";
import { Bucket } from "@winglang/sdk/lib/target-sim/bucket";

import { Platform as SimPlatform } from "@winglang/sdk/lib/target-sim/platform";

describe("compatibility spy", async () => {
let spyPlatform = new Platform();

vi.spyOn(spyPlatform, "newInstance");

const manager = new PlatformManager({
platformPaths: ["sim", join(__dirname, "../lib")],
});

//@ts-expect-error- accessing private method
vi.spyOn(manager, "loadPlatformPath").mockImplementation(
//@ts-expect-error- accessing private method
(platformPath: string) => {
//@ts-expect-error- accessing private property
manager.platformInstances.push(
platformPath === "sim" ? new SimPlatform() : spyPlatform
);
}
);

const app = manager.createApp({
entrypointDir: __dirname,
}) as SimApp;

test("app overrides and hooks set correctly", () => {
expect(app._newInstanceOverrides.length).toBe(1);
//@ts-expect-error - _synthHooks is protected
expect(app._synthHooks?.preSynthesize.length).toBe(1);
});

const bucket = app.newAbstract(BUCKET_FQN, app, "bucket") as Bucket;

bucket.addObject("a", "b");
// @ts-expect-error- accessing private property
bucket.public;

test("each new instance is wrapped in a proxy", () => {
expect(spyPlatform.newInstance).toBeCalledTimes(1);
expect(spyPlatform._usageContext.get("Bucket")).toEqual(
new Set(["addObject", "initialObjects", "public"])
);
});
});
41 changes: 41 additions & 0 deletions libs/compatibility-spy/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./lib",
"declarationMap": false,
"inlineSourceMap": true,
"inlineSources": true,
"alwaysStrict": true,
"declaration": true,
"experimentalDecorators": true,
"incremental": true,
"lib": [
"es2020",
"dom"
],
"esModuleInterop": true,
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"stripInternal": false,
"composite": false,
"tsBuildInfoFile": "lib/tsconfig.tsbuildinfo"
},
"include": [
"./src/**/*"
],
"exclude": [
"./node_modules"
]
}
7 changes: 7 additions & 0 deletions libs/compatibility-spy/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
dts: true,
clean: true,
});
16 changes: 16 additions & 0 deletions libs/compatibility-spy/turbo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://turborepo.org/schema.json",
"extends": ["//"],
"pipeline": {
"compile": {
"outputs": ["dist/**", "lib/**"]
},
"test": {
"dependsOn": ["compile"]
},
"package": {
"dependsOn": ["compile"],
"outputs": ["../../dist/winglang-compatibility-spy-*.tgz"]
}
}
}
10 changes: 9 additions & 1 deletion libs/wingsdk/src/core/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Construct, IConstruct } from "constructs";
import { NotImplementedError } from "./errors";
import { SDK_PACKAGE_NAME } from "../constants";
import { APP_SYMBOL, IApp, Node } from "../std/node";
import type { IResource } from "../std/resource";
import { type IResource } from "../std/resource";
import { TestRunner } from "../std/test-runner";

/**
Expand Down Expand Up @@ -159,6 +159,12 @@ export abstract class App extends Construct implements IApp {
*/
public _testRunner: TestRunner | undefined;

/**
* SynthHooks hooks of dependent platforms
* @internal
*/
protected _synthHooks?: SynthHooks;

constructor(scope: Construct, id: string, props: AppProps) {
super(scope, id);
if (!props.entrypointDir) {
Expand All @@ -172,6 +178,7 @@ export abstract class App extends Construct implements IApp {

this.entrypointDir = props.entrypointDir;
this._newInstanceOverrides = props.newInstanceOverrides ?? [];
this._synthHooks = props.synthHooks;
this.isTestEnvironment = props.isTestEnvironment ?? false;
}

Expand Down Expand Up @@ -283,6 +290,7 @@ export abstract class App extends Construct implements IApp {
if (!type) {
return undefined;
}

return new type(scope, id, ...args);
}
}
Expand Down
Loading

0 comments on commit 0d19f9e

Please sign in to comment.