@microsoft/windows-admin-center-sdk
Version:
Microsoft - Windows Admin Center Shell
530 lines (528 loc) • 21.8 kB
JavaScript
import { of, throwError } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import { CimStream } from '../data/cim-stream';
import { Http } from '../data/http';
import { Net } from '../data/net';
import { PowerShellStream } from '../data/powershell-stream';
import { Logging } from '../diagnostics/logging';
import { RpcObservablePerformanceClient } from '../rpc/performance/rpc-observable-performance-client';
import { RpcObservablePerformanceConfigClient } from '../rpc/performance/rpc-observable-performance-config-client';
import { RpcObservablePerformanceServer } from '../rpc/performance/rpc-observable-performance-server';
import { PerformanceProfileDataType } from './performance-profile-data-type';
import { PerformanceProfileDatabase } from './performance-profile-database';
/**
* Performance measurement class.
*/
export class PerformanceProfile {
static monitorName = 'PerformanceProfile';
static instance;
database = null;
rpc;
rpcObservablePerformanceClient;
rpcObservablePerformanceServer;
subscription;
moduleVersionMap = {};
rpcObservablePerformanceConfigClient;
/**
* Gets the current PerformanceProfile instance.
*/
static get current() {
if (PerformanceProfile.instance) {
return PerformanceProfile.instance;
}
PerformanceProfile.instance = new PerformanceProfile();
return PerformanceProfile.instance;
}
static get database() {
return PerformanceProfile.current.database;
}
/**
* Record Route navigation performance measurement.
*/
static logRouteNavigation(source, start, end, url, target, errorMessage) {
const data = {
source,
start,
end,
errorMessage,
type: PerformanceProfileDataType.RouteNavigation,
routeNavigation: { url, target }
};
PerformanceProfile.current.log(data);
}
/**
* Record Null packet.
*/
static logNull(source) {
const data = {
source,
start: 0,
end: 0,
errorMessage: null,
type: PerformanceProfileDataType.Null
};
PerformanceProfile.current.logAnyway(data);
}
/**
* Record XHR or Fetch performance measurement for PowerShell.
*/
static logXhrFetchPowerShell(source, start, end, url, method, status, powershell, errorMessage) {
const data = {
source,
start,
end,
errorMessage,
type: PerformanceProfileDataType.XhrFetch,
xhrFetch: { url, method, status, powershell }
};
PerformanceProfile.current.log(data);
}
/**
* Record XHR or Fetch performance measurement for CIM.
*/
static logXhrFetchCim(source, start, end, url, method, status, cim, errorMessage) {
const data = {
source,
start,
end,
errorMessage,
type: PerformanceProfileDataType.XhrFetch,
xhrFetch: { url, method, status, cim }
};
PerformanceProfile.current.log(data);
}
/**
* Record XHR or Fetch performance measurement for Batch.
*/
static logXhrFetchBatch(source, start, end, url, method, status, batch, errorMessage) {
const data = {
source,
start,
end,
errorMessage,
type: PerformanceProfileDataType.XhrFetch,
xhrFetch: { url, method, status, batch }
};
PerformanceProfile.current.log(data);
}
/**
* Record XHR or Fetch performance measurement for general.
*/
static logXhrFetch(source, start, end, url, method, status, errorMessage) {
const data = {
source,
start,
end,
errorMessage,
type: PerformanceProfileDataType.XhrFetch,
xhrFetch: { url, method, status }
};
PerformanceProfile.current.log(data);
}
/**
* Record WebSocket performance measurement.
*/
static logWebSocketPowerShell(source, nodeName, command, context, errorMessage) {
const data = {
source,
start: context.progressStart,
end: context.progressEnd,
errorMessage,
type: PerformanceProfileDataType.WebSocket,
webSocket: { nodeName, id: context.id, count: context.count, itemCount: context.itemCount, powershell: { command } }
};
PerformanceProfile.current.log(data);
}
/**
* Record WebSocket performance measurement.
*/
static logWebSocketCim(source, nodeName, cim, context, errorMessage) {
const data = {
source,
start: context.progressStart,
end: context.progressEnd,
errorMessage,
type: PerformanceProfileDataType.WebSocket,
webSocket: { nodeName, id: context.id, count: context.count, itemCount: context.itemCount, cim }
};
PerformanceProfile.current.log(data);
}
static powershellApi(url, body, response) {
const powershell = {};
if (body && body.indexOf('\"properties\"') > 0) {
powershell.command = PerformanceProfile.getBetween(body, ',\"command\":\"', '\",\"')
|| PerformanceProfile.getBetween(body, '\"script\":\"##', '##:');
}
powershell.completed = response && response.completed;
if (!powershell.completed) {
powershell.completed = response && response.properties && response.properties.completed;
}
powershell.sessionId = response && response.sessionId;
if (!powershell.sessionId) {
powershell.sessionId = response && response.properties && response.properties.sessionId;
if (!powershell.sessionId) {
powershell.sessionId = PerformanceProfile.getBetween(url, 'features/powershellApi/pssessions/', '?');
}
}
return powershell;
}
static cimApi(url, body) {
if (url.indexOf('/features/cim/query') > 0) {
try {
return JSON.parse(body);
}
catch {
return null;
}
}
const namespaceName = PerformanceProfile.getBetween(url, '/namespaces/', '/classes/');
const className = PerformanceProfile.getBetween(url, '/classes/', '/instances');
return { namespace: namespaceName, className: className };
}
static batchApi(body, response) {
const delimiter = '\x0d\x0a';
const batch = [];
let last = body.indexOf(delimiter);
let separator = body.substring(0, last + 2);
const bodySegments = body.split(separator);
last = response.indexOf(delimiter);
separator = response.substring(0, last + 2);
const responseSegments = response.split(separator);
if (bodySegments.length === responseSegments.length && bodySegments.length > 1) {
let batchMethod;
let batchUrl;
let batchBody;
let batchStatus;
let batchResponse;
for (let i = 1; i < bodySegments.length; i++) {
const bodySegment = bodySegments[i];
const responseSegment = responseSegments[i];
const bodySegmentLines = bodySegment.split(delimiter);
let emptyLines = 0;
for (let j = 0; j < bodySegmentLines.length; j++) {
const line = bodySegmentLines[j];
if (line.length > 0) {
if (emptyLines === 1 && !batchUrl) {
// reached header segment.
const query = line.split(' ');
batchMethod = query[0];
batchUrl = query[1];
}
else if (emptyLines >= 2 && !batchBody) {
batchBody = line;
}
}
else {
emptyLines++;
}
}
emptyLines = 0;
const responseSegmentLines = responseSegment.split(delimiter);
for (let j = 0; j < responseSegmentLines.length; j++) {
const line = responseSegmentLines[j];
if (line.length > 0) {
if (emptyLines === 1 && !batchStatus) {
// reached header segment.
const query = line.split(' ');
batchStatus = Number(query[1]);
}
else if (emptyLines >= 2 && !batchResponse) {
try {
batchResponse = JSON.parse(line);
}
catch {
batchResponse = {};
}
}
}
else {
emptyLines++;
}
}
const batchItem = {
url: batchUrl,
status: batchStatus,
method: batchMethod
};
if (batchUrl.indexOf('/features/powershellApi/') > 0) {
batchItem.powershell = PerformanceProfile.powershellApi(batchUrl, batchBody, batchResponse);
}
else if (batchUrl.includes('/features/cim/query')) {
batchItem.cim = PerformanceProfile.cimApi(batchUrl, batchBody);
}
batch.push(batchItem);
}
}
return batch;
}
static getBetween(source, begin, end) {
let index0 = source.indexOf(begin);
if (index0 < 0) {
return null;
}
index0 += begin.length;
const index1 = source.indexOf(end, index0);
if (index1 < 0) {
return null;
}
return source.substring(index0, index1 - index0);
}
enable(rpc) {
MsftSme.setPerformanceProfile(true);
this.registerRpc(rpc);
}
disable(rpc) {
MsftSme.setPerformanceProfile(false);
this.registerRpc(rpc);
}
checkPerformanceProfile() {
if (!MsftSme.getPerformanceProfile()) {
if (this.database) {
this.database.close();
this.database = null;
}
return false;
}
if (!this.database) {
this.database = new PerformanceProfileDatabase();
}
return true;
}
registerRpc(rpc) {
if (!this.rpc) {
this.rpc = rpc;
if (MsftSme.isShell()) {
this.subscription = rpc.stateChanged
.pipe(filter(active => active), take(1))
.subscribe(() => {
this.rpcObservablePerformanceConfigClient = new RpcObservablePerformanceConfigClient(this.rpc);
this.rpcObservablePerformanceServer = new RpcObservablePerformanceServer(rpc);
this.rpcObservablePerformanceServer.register(request => {
if (request.type === PerformanceProfileDataType.Null) {
// The packet is null packet and retain only the version data.
if (request.sourceVersion === '0.2.0') {
// this version can support config command when on/off switch was used.
this.moduleVersionMap[request.sourceName] = request.sourceVersion;
}
return of(null);
}
if (!this.checkPerformanceProfile()) {
return of(null);
}
return this.database.write(request).pipe(map(() => null));
});
});
}
else {
this.subscription = rpc.stateChanged
.pipe(filter(active => active), take(1))
.subscribe(() => {
this.rpcObservablePerformanceClient = new RpcObservablePerformanceClient(rpc);
});
}
}
this.updateMonitors();
}
updateMonitors() {
const enabled = this.checkPerformanceProfile();
if (this.rpcObservablePerformanceConfigClient) {
// enable/disable the performance profile data collection to current modules.
// but these must be version 0.2.0.
const items = this.rpc.rpcManager.getCurrentRpcOutbound();
if (items) {
for (const item of items) {
if (this.moduleVersionMap[item.name]) {
this.rpcObservablePerformanceConfigClient.config({ enabled }, item).subscribe();
}
}
}
}
if (enabled) {
this.registerHttp();
this.registerPowerShellStream();
this.registerCimStream();
}
else {
Http.unregisterMonitors(PerformanceProfile.monitorName);
PowerShellStream.unregisterMonitors(PerformanceProfile.monitorName);
CimStream.unregisterMonitors(PerformanceProfile.monitorName);
}
}
log(message) {
if (!this.checkPerformanceProfile()) {
return;
}
const self = MsftSme.self();
const record = {
...message,
...{ sessionId: self.Init.sessionId, timestamp: Date.now(), moduleName: self.Init.moduleName }
};
if (!record.errorMessage) {
delete record['errorMessage'];
}
if (this.rpc && this.rpc.stateActive && this.rpcObservablePerformanceClient) {
// send to shell.
this.rpcObservablePerformanceClient.log(record).subscribe();
}
else {
// write to database.
this.database.write(record).subscribe();
}
}
logAnyway(message) {
const self = MsftSme.self();
const record = {
...message,
...{ sessionId: self.Init.sessionId, timestamp: Date.now(), moduleName: self.Init.moduleName }
};
if (!record.errorMessage) {
delete record['errorMessage'];
}
if (this.rpc && this.rpc.stateActive && this.rpcObservablePerformanceClient) {
// send to shell.
this.rpcObservablePerformanceClient.log(record).subscribe();
}
}
registerHttp() {
const startPropertyName = '_start_';
Http.registerMonitorSet({
name: PerformanceProfile.monitorName,
preMonitor: (request) => {
if (!request) {
Logging.logWarning('Http', 'Http performance profile measurement error to access the AjaxRequest object.');
return of(request);
}
request[startPropertyName] = Date.now();
return of(request);
},
successMonitor: (response) => {
if (!response || !response.request || !response.request.url) {
Logging.logWarning('Http', 'Http performance profile measurement error to access the AjaxResponse object.');
return of(response);
}
const request = response.request;
const url = request.url;
const method = request.method;
const end = Date.now();
const start = request[startPropertyName];
if (url.indexOf('/features/powershellApi/') > 0) {
const powershell = PerformanceProfile.powershellApi(url, request.body, response.response);
PerformanceProfile.logXhrFetchPowerShell('Http', start, end, url, method, response.status, powershell);
}
else if (url.includes('/features/cim/')) {
const cim = PerformanceProfile.cimApi(url, request.body);
PerformanceProfile.logXhrFetchCim('Http', start, end, url, method, response.status, cim);
}
else if (url.indexOf('/api/batch') > 0) {
const batch = PerformanceProfile.batchApi(request.body, response.response);
PerformanceProfile.logXhrFetchBatch('Http', start, end, url, method, response.status, batch);
}
else {
PerformanceProfile.logXhrFetch('Http', start, end, url, method, response.status);
}
return of(response);
},
errorMonitor: (error) => {
if (!error || !error.response || !error.request || !error.request.url) {
Logging.logWarning('Http', 'Http performance profile measurement error to access the AjaxError object.');
return throwError(() => error);
}
const request = error.request;
const url = request.url;
const method = request.method;
const end = Date.now();
const start = request[startPropertyName];
const message = Net.getErrorMessage(error);
if (url.indexOf('features/powershellApi') > 0) {
const powershell = PerformanceProfile.powershellApi(url, request.body, error.response);
PerformanceProfile.logXhrFetchPowerShell('Http', start, end, url, method, error.status, powershell, message);
}
else if (url.includes('/features/cim/')) {
const cim = PerformanceProfile.cimApi(url, request.body);
PerformanceProfile.logXhrFetchCim('Http', start, end, url, method, error.status, cim, message);
}
else {
PerformanceProfile.logXhrFetch('Http', start, end, url, method, error.status, message);
}
return throwError(() => error);
}
});
}
registerPowerShellStream() {
let masterId = 1;
PowerShellStream.registerMonitorSet({
name: PerformanceProfile.monitorName,
preMonitor: (nodeName, command, options) => {
const start = Date.now();
const id = masterId++;
const context = {
nodeName,
command,
options,
data: { id, start, progressStart: start, count: 0, itemCount: 0 }
};
return of(context);
},
successMonitor: (response, context) => {
context.data.progressEnd = Date.now();
context.data.count++;
context.data.itemCount += (response && response.results && response.results.length || 1);
PerformanceProfile.logWebSocketPowerShell('PowerShellStream', context.nodeName, context.command.command, context.data);
context.data.progressStart = Date.now();
return of(response);
},
errorMonitor: (error, context) => {
context.data.progressEnd = Date.now();
PerformanceProfile.logWebSocketPowerShell('PowerShellStream', context.nodeName, context.command.command, context.data, Net.getErrorMessage(error));
return throwError(() => error);
}
});
}
registerCimStream() {
let masterId = 1;
CimStream.registerMonitorSet({
name: PerformanceProfile.monitorName,
preMonitor: (nodeName, requestState, request, format, options) => {
const start = Date.now();
const id = masterId++;
const context = {
nodeName,
requestState,
request,
format,
options,
data: { id, start, progressStart: start, count: 0, itemCount: 0 }
};
return of(context);
},
successMonitor: (response, context) => {
context.data.progressEnd = Date.now();
context.data.count++;
context.data.itemCount += (response && response.results && response.results.length || 1);
PerformanceProfile.logWebSocketCim('CimStream', context.nodeName, this.removeCimStreamDetailData(context.request), context.data);
context.data.progressStart = Date.now();
return of(response);
},
errorMonitor: (error, context) => {
context.data.progressEnd = Date.now();
PerformanceProfile.logWebSocketCim('CimStream', context.nodeName, this.removeCimStreamDetailData(context.request), context.data, Net.getErrorMessage(error));
return throwError(() => error);
}
});
}
removeCimStreamDetailData(request) {
const skipKeys = ['data', 'keyProperties'];
const requestRaw = request;
const keys = Object.keys(requestRaw);
const trimmedKeys = keys.filter(key => skipKeys.indexOf(key) < 0);
if (keys.length !== trimmedKeys.length) {
const copy = {};
for (const key of trimmedKeys) {
copy[key] = requestRaw[key];
}
return copy;
}
return request;
}
}
//# sourceMappingURL=performance-profile.js.map