microsoft/vscode-react-native

Public

mirrored from https://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
34472878f9e8d227bd5d0902161c571864c5d12d

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/experimentService/experimentService.ts

183lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as Configstore from "configstore";
5import * as https from "https";
6import * as vscode from "vscode";
7import { IExperiment } from "./IExperiment";
8import { PromiseUtil } from "../../common/node/promise";
9import { TelemetryHelper } from "../../common/telemetryHelper";
10import { Telemetry } from "../../common/telemetry";
11
12export enum ExperimentStatuses {
13 ENABLED = "enabled",
14 DISABLED = "disabled",
15 FAILED = "failed",
16}
17
18export interface ExperimentConfig {
19 experimentName: string;
20 popCoveragePercent: number;
21 enabled: boolean;
22}
23
24export interface ExperimentParameters extends ExperimentConfig {
25 [key: string]: any;
26 extensionId?: string;
27}
28
29export interface ExperimentResult {
30 resultStatus: ExperimentStatuses;
31 updatedExperimentParameters: ExperimentParameters;
32 error?: Error;
33}
34
35export class ExperimentService implements vscode.Disposable {
36 private static instance: ExperimentService;
37
38 private readonly endpointURL: string;
39 private readonly configName: string;
40 private config: Configstore;
41 private downloadedExperimentsConfig: Array<ExperimentConfig> | null;
42 private experimentsInstances: Map<string, IExperiment>;
43 private downloadConfigRequest: Promise<ExperimentConfig[]>;
44 private cancellationTokenSource: vscode.CancellationTokenSource;
45
46 public static create(): ExperimentService {
47 if (!ExperimentService.instance) {
48 ExperimentService.instance = new ExperimentService();
49 }
50
51 return ExperimentService.instance;
52 }
53
54 public async runExperiments(): Promise<void> {
55 if (!this.downloadedExperimentsConfig) {
56 this.downloadedExperimentsConfig = await this.downloadConfigRequest;
57 this.experimentsInstances = await this.initializeExperimentsInstances();
58 }
59
60 let experimentResults: Array<ExperimentResult> = await Promise.all(
61 this.downloadedExperimentsConfig.map(expConfig => this.executeExperiment(expConfig)),
62 );
63
64 this.sendExperimentTelemetry(experimentResults);
65 }
66
67 public dispose(): void {
68 this.cancellationTokenSource.cancel();
69 this.cancellationTokenSource.dispose();
70 }
71
72 private constructor() {
73 this.endpointURL =
74 "https://microsoft.github.io/vscode-react-native/experiments/experimentsConfig.json";
75 this.configName = "reactNativeToolsConfig";
76
77 this.config = new Configstore(this.configName);
78 this.cancellationTokenSource = new vscode.CancellationTokenSource();
79 this.downloadedExperimentsConfig = null;
80
81 this.downloadConfigRequest = this.retryDownloadExperimentsConfig();
82 }
83
84 private async executeExperiment(expConfig: ExperimentConfig): Promise<ExperimentResult> {
85 let curExperimentParameters = this.config.get(expConfig.experimentName);
86 let expInstance = this.experimentsInstances.get(expConfig.experimentName);
87
88 let expResult: ExperimentResult;
89 if (expInstance && expConfig.enabled) {
90 try {
91 expResult = await expInstance.run(expConfig, curExperimentParameters);
92 this.config.set(expConfig.experimentName, expResult.updatedExperimentParameters);
93 } catch (err) {
94 expResult = {
95 resultStatus: ExperimentStatuses.FAILED,
96 updatedExperimentParameters: expConfig,
97 error: err,
98 };
99 }
100 } else {
101 expResult = {
102 resultStatus: ExperimentStatuses.DISABLED,
103 updatedExperimentParameters: expConfig,
104 };
105 }
106
107 return expResult;
108 }
109
110 private async retryDownloadExperimentsConfig(retryCount = 60): Promise<ExperimentConfig[]> {
111 try {
112 return await this.downloadExperimentsConfig();
113 } catch (err) {
114 if (retryCount < 1 || this.cancellationTokenSource.token.isCancellationRequested) {
115 throw err;
116 }
117
118 await PromiseUtil.delay(2000);
119 return await this.retryDownloadExperimentsConfig(--retryCount);
120 }
121 }
122
123 private async initializeExperimentsInstances(): Promise<Map<string, IExperiment>> {
124 let expInstances = new Map<string, IExperiment>();
125
126 if (this.downloadedExperimentsConfig) {
127 for (let expConfig of this.downloadedExperimentsConfig) {
128 try {
129 let expClass = await import(`./experiments/${expConfig.experimentName}`);
130 expInstances.set(expConfig.experimentName, new expClass.default());
131 } catch (err) {
132 expConfig.enabled = false;
133 }
134 }
135 }
136
137 return expInstances;
138 }
139
140 private downloadExperimentsConfig(): Promise<ExperimentConfig[]> {
141 return new Promise<ExperimentConfig[]>((resolve, reject) => {
142 https
143 .get(this.endpointURL, response => {
144 let data = "";
145 response.setEncoding("utf8");
146 response.on("data", (chunk: string) => (data += chunk));
147 response.on("end", () => {
148 try {
149 resolve(JSON.parse(data));
150 } catch (err) {
151 reject(err);
152 }
153 });
154 response.on("error", reject);
155 })
156 .on("error", reject);
157 });
158 }
159
160 private sendExperimentTelemetry(experimentsResults: ExperimentResult[]): void {
161 const runExperimentsEvent = TelemetryHelper.createTelemetryEvent("runExperiments");
162
163 experimentsResults.forEach(expResult => {
164 if (expResult.resultStatus === ExperimentStatuses.FAILED && expResult.error) {
165 TelemetryHelper.addTelemetryEventErrorProperty(
166 runExperimentsEvent,
167 expResult.error,
168 undefined,
169 `${expResult.updatedExperimentParameters.experimentName}.`,
170 );
171 } else {
172 TelemetryHelper.addTelemetryEventProperty(
173 runExperimentsEvent,
174 expResult.updatedExperimentParameters.experimentName,
175 expResult.resultStatus,
176 false,
177 );
178 }
179 });
180
181 Telemetry.send(runExperimentsEvent);
182 }
183}
184