@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
519 lines (518 loc) • 18.4 kB
JavaScript
var __decorate =
(this && this.__decorate) ||
function (decorators, target, key, desc) {
var c = arguments.length,
r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
d;
if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
r = Reflect.decorate(decorators, target, key, desc);
else
for (var i = decorators.length - 1; i >= 0; i--)
if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param =
(this && this.__param) ||
function (paramIndex, decorator) {
return function (target, key) {
decorator(target, key, paramIndex);
};
};
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as osLib from 'os';
import { Promises } from '@sussudio/base/common/async.mjs';
import { getNodeType, parse } from '@sussudio/base/common/json.mjs';
import { Schemas } from '@sussudio/base/common/network.mjs';
import { basename, join } from '@sussudio/base/common/path.mjs';
import { isLinux, isWindows } from '@sussudio/base/common/platform.mjs';
import { URI } from '@sussudio/base/common/uri.mjs';
import { virtualMachineHint } from '@sussudio/base/node/id.mjs';
import { Promises as pfs } from '@sussudio/base/node/pfs.mjs';
import { listProcesses } from '@sussudio/base/node/ps.mjs';
import { isRemoteDiagnosticError } from '../common/diagnostics.mjs';
import { ByteSize } from '../../files/common/files.mjs';
import { IProductService } from '../../product/common/productService.mjs';
import { ITelemetryService } from '../../telemetry/common/telemetry.mjs';
const worksapceStatsCache = new Map();
export async function collectWorkspaceStats(folder, filter) {
const cacheKey = `${folder}::${filter.join(':')}`;
const cached = worksapceStatsCache.get(cacheKey);
if (cached) {
return cached;
}
const configFilePatterns = [
{ tag: 'grunt.js', filePattern: /^gruntfile\.js$/i },
{ tag: 'gulp.js', filePattern: /^gulpfile\.js$/i },
{ tag: 'tsconfig.json', filePattern: /^tsconfig\.json$/i },
{ tag: 'package.json', filePattern: /^package\.json$/i },
{ tag: 'jsconfig.json', filePattern: /^jsconfig\.json$/i },
{ tag: 'tslint.json', filePattern: /^tslint\.json$/i },
{ tag: 'eslint.json', filePattern: /^eslint\.json$/i },
{ tag: 'tasks.json', filePattern: /^tasks\.json$/i },
{ tag: 'launch.json', filePattern: /^launch\.json$/i },
{ tag: 'settings.json', filePattern: /^settings\.json$/i },
{ tag: 'webpack.config.js', filePattern: /^webpack\.config\.js$/i },
{ tag: 'project.json', filePattern: /^project\.json$/i },
{ tag: 'makefile', filePattern: /^makefile$/i },
{ tag: 'sln', filePattern: /^.+\.sln$/i },
{ tag: 'csproj', filePattern: /^.+\.csproj$/i },
{ tag: 'cmake', filePattern: /^.+\.cmake$/i },
{ tag: 'github-actions', filePattern: /^.+\.ya?ml$/i, relativePathPattern: /^\.github(?:\/|\\)workflows$/i },
{ tag: 'devcontainer.json', filePattern: /^devcontainer\.json$/i },
{ tag: 'dockerfile', filePattern: /^(dockerfile|docker\-compose\.ya?ml)$/i },
];
const fileTypes = new Map();
const configFiles = new Map();
const MAX_FILES = 20000;
function collect(root, dir, filter, token) {
const relativePath = dir.substring(root.length + 1);
return Promises.withAsyncBody(async (resolve) => {
let files;
try {
files = await pfs.readdir(dir, { withFileTypes: true });
} catch (error) {
// Ignore folders that can't be read
resolve();
return;
}
if (token.count >= MAX_FILES) {
token.count += files.length;
token.maxReached = true;
resolve();
return;
}
let pending = files.length;
if (pending === 0) {
resolve();
return;
}
let filesToRead = files;
if (token.count + files.length > MAX_FILES) {
token.maxReached = true;
pending = MAX_FILES - token.count;
filesToRead = files.slice(0, pending);
}
token.count += files.length;
for (const file of filesToRead) {
if (file.isDirectory()) {
if (!filter.includes(file.name)) {
await collect(root, join(dir, file.name), filter, token);
}
if (--pending === 0) {
resolve();
return;
}
} else {
const index = file.name.lastIndexOf('.');
if (index >= 0) {
const fileType = file.name.substring(index + 1);
if (fileType) {
fileTypes.set(fileType, (fileTypes.get(fileType) ?? 0) + 1);
}
}
for (const configFile of configFilePatterns) {
if (
configFile.relativePathPattern?.test(relativePath) !== false &&
configFile.filePattern.test(file.name)
) {
configFiles.set(configFile.tag, (configFiles.get(configFile.tag) ?? 0) + 1);
}
}
if (--pending === 0) {
resolve();
return;
}
}
}
});
}
const statsPromise = Promises.withAsyncBody(async (resolve) => {
const token = { count: 0, maxReached: false };
await collect(folder, folder, filter, token);
const launchConfigs = await collectLaunchConfigs(folder);
resolve({
configFiles: asSortedItems(configFiles),
fileTypes: asSortedItems(fileTypes),
fileCount: token.count,
maxFilesReached: token.maxReached,
launchConfigFiles: launchConfigs,
});
});
worksapceStatsCache.set(cacheKey, statsPromise);
return statsPromise;
}
function asSortedItems(items) {
return Array.from(items.entries(), ([name, count]) => ({ name: name, count: count })).sort(
(a, b) => b.count - a.count,
);
}
export function getMachineInfo() {
const machineInfo = {
os: `${osLib.type()} ${osLib.arch()} ${osLib.release()}`,
memory: `${(osLib.totalmem() / ByteSize.GB).toFixed(2)}GB (${(osLib.freemem() / ByteSize.GB).toFixed(2)}GB free)`,
vmHint: `${Math.round(virtualMachineHint.value() * 100)}%`,
};
const cpus = osLib.cpus();
if (cpus && cpus.length > 0) {
machineInfo.cpus = `${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`;
}
return machineInfo;
}
export async function collectLaunchConfigs(folder) {
try {
const launchConfigs = new Map();
const launchConfig = join(folder, '.vscode', 'launch.json');
const contents = await pfs.readFile(launchConfig);
const errors = [];
const json = parse(contents.toString(), errors);
if (errors.length) {
console.log(`Unable to parse ${launchConfig}`);
return [];
}
if (getNodeType(json) === 'object' && json['configurations']) {
for (const each of json['configurations']) {
const type = each['type'];
if (type) {
if (launchConfigs.has(type)) {
launchConfigs.set(type, launchConfigs.get(type) + 1);
} else {
launchConfigs.set(type, 1);
}
}
}
}
return asSortedItems(launchConfigs);
} catch (error) {
return [];
}
}
let DiagnosticsService = class DiagnosticsService {
telemetryService;
productService;
constructor(telemetryService, productService) {
this.telemetryService = telemetryService;
this.productService = productService;
}
formatMachineInfo(info) {
const output = [];
output.push(`OS Version: ${info.os}`);
output.push(`CPUs: ${info.cpus}`);
output.push(`Memory (System): ${info.memory}`);
output.push(`VM: ${info.vmHint}`);
return output.join('\n');
}
formatEnvironment(info) {
const output = [];
output.push(
`Version: ${this.productService.nameShort} ${this.productService.version} (${
this.productService.commit || 'Commit unknown'
}, ${this.productService.date || 'Date unknown'})`,
);
output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);
const cpus = osLib.cpus();
if (cpus && cpus.length > 0) {
output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`);
}
output.push(
`Memory (System): ${(osLib.totalmem() / ByteSize.GB).toFixed(2)}GB (${(osLib.freemem() / ByteSize.GB).toFixed(
2,
)}GB free)`,
);
if (!isWindows) {
output.push(
`Load (avg): ${osLib
.loadavg()
.map((l) => Math.round(l))
.join(', ')}`,
); // only provided on Linux/macOS
}
output.push(`VM: ${Math.round(virtualMachineHint.value() * 100)}%`);
output.push(`Screen Reader: ${info.screenReader ? 'yes' : 'no'}`);
output.push(`Process Argv: ${info.mainArguments.join(' ')}`);
output.push(`GPU Status: ${this.expandGPUFeatures(info.gpuFeatureStatus)}`);
return output.join('\n');
}
async getPerformanceInfo(info, remoteData) {
return Promise.all([listProcesses(info.mainPID), this.formatWorkspaceMetadata(info)]).then(async (result) => {
let [rootProcess, workspaceInfo] = result;
let processInfo = this.formatProcessList(info, rootProcess);
remoteData.forEach((diagnostics) => {
if (isRemoteDiagnosticError(diagnostics)) {
processInfo += `\n${diagnostics.errorMessage}`;
workspaceInfo += `\n${diagnostics.errorMessage}`;
} else {
processInfo += `\n\nRemote: ${diagnostics.hostName}`;
if (diagnostics.processes) {
processInfo += `\n${this.formatProcessList(info, diagnostics.processes)}`;
}
if (diagnostics.workspaceMetadata) {
workspaceInfo += `\n| Remote: ${diagnostics.hostName}`;
for (const folder of Object.keys(diagnostics.workspaceMetadata)) {
const metadata = diagnostics.workspaceMetadata[folder];
let countMessage = `${metadata.fileCount} files`;
if (metadata.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
workspaceInfo += `| Folder (${folder}): ${countMessage}`;
workspaceInfo += this.formatWorkspaceStats(metadata);
}
}
}
});
return {
processInfo,
workspaceInfo,
};
});
}
async getSystemInfo(info, remoteData) {
const { memory, vmHint, os, cpus } = getMachineInfo();
const systemInfo = {
os,
memory,
cpus,
vmHint,
processArgs: `${info.mainArguments.join(' ')}`,
gpuStatus: info.gpuFeatureStatus,
screenReader: `${info.screenReader ? 'yes' : 'no'}`,
remoteData,
};
if (!isWindows) {
systemInfo.load = `${osLib
.loadavg()
.map((l) => Math.round(l))
.join(', ')}`;
}
if (isLinux) {
systemInfo.linuxEnv = {
desktopSession: process.env['DESKTOP_SESSION'],
xdgSessionDesktop: process.env['XDG_SESSION_DESKTOP'],
xdgCurrentDesktop: process.env['XDG_CURRENT_DESKTOP'],
xdgSessionType: process.env['XDG_SESSION_TYPE'],
};
}
return Promise.resolve(systemInfo);
}
async getDiagnostics(info, remoteDiagnostics) {
const output = [];
return listProcesses(info.mainPID).then(async (rootProcess) => {
// Environment Info
output.push('');
output.push(this.formatEnvironment(info));
// Process List
output.push('');
output.push(this.formatProcessList(info, rootProcess));
// Workspace Stats
if (info.windows.some((window) => window.folderURIs && window.folderURIs.length > 0 && !window.remoteAuthority)) {
output.push('');
output.push('Workspace Stats: ');
output.push(await this.formatWorkspaceMetadata(info));
}
remoteDiagnostics.forEach((diagnostics) => {
if (isRemoteDiagnosticError(diagnostics)) {
output.push(`\n${diagnostics.errorMessage}`);
} else {
output.push('\n\n');
output.push(`Remote: ${diagnostics.hostName}`);
output.push(this.formatMachineInfo(diagnostics.machineInfo));
if (diagnostics.processes) {
output.push(this.formatProcessList(info, diagnostics.processes));
}
if (diagnostics.workspaceMetadata) {
for (const folder of Object.keys(diagnostics.workspaceMetadata)) {
const metadata = diagnostics.workspaceMetadata[folder];
let countMessage = `${metadata.fileCount} files`;
if (metadata.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
output.push(`Folder (${folder}): ${countMessage}`);
output.push(this.formatWorkspaceStats(metadata));
}
}
}
});
output.push('');
output.push('');
return output.join('\n');
});
}
formatWorkspaceStats(workspaceStats) {
const output = [];
const lineLength = 60;
let col = 0;
const appendAndWrap = (name, count) => {
const item = ` ${name}(${count})`;
if (col + item.length > lineLength) {
output.push(line);
line = '| ';
col = line.length;
} else {
col += item.length;
}
line += item;
};
// File Types
let line = '| File types:';
const maxShown = 10;
const max = workspaceStats.fileTypes.length > maxShown ? maxShown : workspaceStats.fileTypes.length;
for (let i = 0; i < max; i++) {
const item = workspaceStats.fileTypes[i];
appendAndWrap(item.name, item.count);
}
output.push(line);
// Conf Files
if (workspaceStats.configFiles.length >= 0) {
line = '| Conf files:';
col = 0;
workspaceStats.configFiles.forEach((item) => {
appendAndWrap(item.name, item.count);
});
output.push(line);
}
if (workspaceStats.launchConfigFiles.length > 0) {
let line = '| Launch Configs:';
workspaceStats.launchConfigFiles.forEach((each) => {
const item = each.count > 1 ? ` ${each.name}(${each.count})` : ` ${each.name}`;
line += item;
});
output.push(line);
}
return output.join('\n');
}
expandGPUFeatures(gpuFeatures) {
const longestFeatureName = Math.max(...Object.keys(gpuFeatures).map((feature) => feature.length));
// Make columns aligned by adding spaces after feature name
return Object.keys(gpuFeatures)
.map((feature) => `${feature}: ${' '.repeat(longestFeatureName - feature.length)} ${gpuFeatures[feature]}`)
.join('\n ');
}
formatWorkspaceMetadata(info) {
const output = [];
const workspaceStatPromises = [];
info.windows.forEach((window) => {
if (window.folderURIs.length === 0 || !!window.remoteAuthority) {
return;
}
output.push(`| Window (${window.title})`);
window.folderURIs.forEach((uriComponents) => {
const folderUri = URI.revive(uriComponents);
if (folderUri.scheme === Schemas.file) {
const folder = folderUri.fsPath;
workspaceStatPromises.push(
collectWorkspaceStats(folder, ['node_modules', '.git'])
.then((stats) => {
let countMessage = `${stats.fileCount} files`;
if (stats.maxFilesReached) {
countMessage = `more than ${countMessage}`;
}
output.push(`| Folder (${basename(folder)}): ${countMessage}`);
output.push(this.formatWorkspaceStats(stats));
})
.catch((error) => {
output.push(
`| Error: Unable to collect workspace stats for folder ${folder} (${error.toString()})`,
);
}),
);
} else {
output.push(`| Folder (${folderUri.toString()}): Workspace stats not available.`);
}
});
});
return Promise.all(workspaceStatPromises)
.then((_) => output.join('\n'))
.catch((e) => `Unable to collect workspace stats: ${e}`);
}
formatProcessList(info, rootProcess) {
const mapPidToWindowTitle = new Map();
info.windows.forEach((window) => mapPidToWindowTitle.set(window.pid, window.title));
const output = [];
output.push('CPU %\tMem MB\t PID\tProcess');
if (rootProcess) {
this.formatProcessItem(info.mainPID, mapPidToWindowTitle, output, rootProcess, 0);
}
return output.join('\n');
}
formatProcessItem(mainPid, mapPidToWindowTitle, output, item, indent) {
const isRoot = indent === 0;
// Format name with indent
let name;
if (isRoot) {
name = item.pid === mainPid ? `${this.productService.applicationName} main` : 'remote agent';
} else {
name = `${' '.repeat(indent)} ${item.name}`;
if (item.name === 'window') {
name = `${name} (${mapPidToWindowTitle.get(item.pid)})`;
}
}
const memory = process.platform === 'win32' ? item.mem : osLib.totalmem() * (item.mem / 100);
output.push(
`${item.load.toFixed(0).padStart(5, ' ')}\t${(memory / ByteSize.MB).toFixed(0).padStart(6, ' ')}\t${item.pid
.toFixed(0)
.padStart(6, ' ')}\t${name}`,
);
// Recurse into children if any
if (Array.isArray(item.children)) {
item.children.forEach((child) => this.formatProcessItem(mainPid, mapPidToWindowTitle, output, child, indent + 1));
}
}
async getWorkspaceFileExtensions(workspace) {
const items = new Set();
for (const { uri } of workspace.folders) {
const folderUri = URI.revive(uri);
if (folderUri.scheme !== Schemas.file) {
continue;
}
const folder = folderUri.fsPath;
try {
const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']);
stats.fileTypes.forEach((item) => items.add(item.name));
} catch {}
}
return { extensions: [...items] };
}
async reportWorkspaceStats(workspace) {
for (const { uri } of workspace.folders) {
const folderUri = URI.revive(uri);
if (folderUri.scheme !== Schemas.file) {
continue;
}
const folder = folderUri.fsPath;
try {
const stats = await collectWorkspaceStats(folder, ['node_modules', '.git']);
this.telemetryService.publicLog2('workspace.stats', {
'workspace.id': workspace.telemetryId,
rendererSessionId: workspace.rendererSessionId,
});
stats.fileTypes.forEach((e) => {
this.telemetryService.publicLog2('workspace.stats.file', {
rendererSessionId: workspace.rendererSessionId,
type: e.name,
count: e.count,
});
});
stats.launchConfigFiles.forEach((e) => {
this.telemetryService.publicLog2('workspace.stats.launchConfigFile', {
rendererSessionId: workspace.rendererSessionId,
type: e.name,
count: e.count,
});
});
stats.configFiles.forEach((e) => {
this.telemetryService.publicLog2('workspace.stats.configFiles', {
rendererSessionId: workspace.rendererSessionId,
type: e.name,
count: e.count,
});
});
} catch {
// Report nothing if collecting metadata fails.
}
}
}
};
DiagnosticsService = __decorate([__param(0, ITelemetryService), __param(1, IProductService)], DiagnosticsService);
export { DiagnosticsService };