diff --git a/extension.js b/extension.js index 4266882..af363d2 100644 --- a/extension.js +++ b/extension.js @@ -2,6 +2,8 @@ // Import the module and reference it with the alias vscode in your code below const path = require('path'); const vscode = require('vscode'); +const analytics = require('./langserver/analytics'); +const package_json = require('./package.json'); const { LanguageClient, TransportKind, } = require('vscode-languageclient'); const { AndroidContentProvider } = require('./src/contentprovider'); const { openLogcatWindow } = require('./src/logcat'); @@ -43,6 +45,8 @@ async function createLanguageClient(context) { } const sourceFiles = (await vscode.workspace.findFiles(`${globSearchRoot}**/*.java`, null, 1000, null)).map(uri => uri.toString()); + const mpids = analytics.getIDs(context); + // Options to control the language client /** @type {import('vscode-languageclient').LanguageClientOptions} */ let clientOptions = { @@ -53,8 +57,11 @@ async function createLanguageClient(context) { initializationOptions: { // extensionPath points to the root of the extension (the folder where this file is) extensionPath: context.extensionPath, + mpuid: mpids.uid, + mpsid: mpids.sid, initialSettings: config, sourceFiles, + vscodeVersion: vscode.version, workspaceFolders: (vscode.workspace.workspaceFolders || []).map(z => z.uri.toString()), }, synchronize: { @@ -107,6 +114,9 @@ function activate(context) { /* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */ AndroidContentProvider.register(context, vscode.workspace); + const mpids = analytics.getIDs(context); + analytics.init(undefined, mpids.uid, mpids.sid, package_json, { vscode_version: vscode.version }); + createLanguageClient(context).then(client => { languageClient = client; refreshLanguageServerEnabledState(); diff --git a/langserver/analytics.js b/langserver/analytics.js new file mode 100644 index 0000000..2bceff4 --- /dev/null +++ b/langserver/analytics.js @@ -0,0 +1,134 @@ +let mp; +/** @type {string} */ +let uid; +/** @type {string} */ +let sid; +/** @type {Map} */ +const timeLabels = new Map(); +let session_start = Date.now(); + +/** + * @param {string} t + * @param {string} u + * @param {string} s + * @param {{name:string,version:string}} package_json + */ +function init(t = '0cca95950055c6553804a46ce7e3df18', u, s, package_json, props) { + if (mp) { + return; + } + try { + mp = require('mixpanel').init(t); + } + catch {} + uid = u; + sid = s; + + const os = require('os'); + event(`${package_json.name}-start`, { + extension: package_json.name, + ext_version: package_json.version, + arch: process.arch, + cpus: os.cpus().length, + mem: (os.totalmem() / 1e6)|0, + platform: process.platform, + node_version: process.version, + release: os.release(), + ...props + }); +} + +/** + * + * @param {string} eventName + * @param {*} [properties] + */ +function event(eventName, properties) { + if (!mp) { + return; + } + try { + if (uid) { + mp.track(eventName, { + distinct_id: uid, + session_id: sid, + session_length: Math.trunc((Date.now() - session_start) / 60e3), + ...properties, + }); + } else { + mp.track(eventName); + } + } catch {} +} + +/** + * @param {string} label + */ +function time(label) { + if (!label || timeLabels.has(label)) { + return; + } + timeLabels.set(label, process.hrtime()); +} + +/** + * @param {string} label + * @param {'ns'|'us'|'ms'|'s'} time_unit + * @param {*} [additionalProps] + */ +function timeEnd(label, time_unit = 'ms', additionalProps = {}) { + if (!label) { + return; + } + const startTime = timeLabels.get(label); + timeLabels.delete(label); + if (!Array.isArray(startTime)) { + return; + } + const elapsed = process.hrtime(startTime); + const count = time_unit === 's' ? elapsed[0] : ((elapsed[0]*1e9) + elapsed[1]); + const divs = { + ns: 1, us: 1e3, ms: 1e6, s: 1 + } + const props = { + [`${label}-elapsed`]: Math.trunc(count / (divs[time_unit] || 1)), + [`${label}-elapsed_unit`]: time_unit, + ...additionalProps, + } + event(label, props); +} + +/** + * @param {import('vscode').ExtensionContext} context + */ +function getIDs(context) { + if (!context || !context.globalState) { + return { + uid: '', sid: '' + }; + } + let uuidv4 = () => { + try { + uuidv4 = require('uuid').v4; + return uuidv4(); + } catch { + return ''; + } + } + let u = uid || (uid = context.globalState.get('mix-panel-id')); + if (typeof u !== 'string' || u.length > 36) { + u = uid = uuidv4(); + context.globalState.update('mix-panel-id', u); + } + let s = sid || (sid = uuidv4()); + return { + uid: u, + sid: s, + } +} + +exports.init = init; +exports.event = event; +exports.time = time; +exports.timeEnd = timeEnd; +exports.getIDs = getIDs; diff --git a/langserver/completions.js b/langserver/completions.js index c0b3482..e48d002 100644 --- a/langserver/completions.js +++ b/langserver/completions.js @@ -5,6 +5,7 @@ const { SourceType } = require('./java/source-types'); const { indexAt } = require('./document'); const { formatDoc } = require('./doc-formatter'); const { trace } = require('./logging'); +const { event } = require('./analytics'); /** * Case-insensitive sort routines @@ -276,6 +277,8 @@ let defaultCompletionTypes = null; /** @type {Map} */ let lastCompletionTypeMap = null; +let completionRequestCount = 0; + function initDefaultCompletionTypes(lib) { defaultCompletionTypes = { instances: 'this super'.split(' ').map(t => ({ @@ -365,6 +368,11 @@ async function getCompletionItems(params, liveParsers, androidLibrary) { } } + completionRequestCount += 1; + if ((completionRequestCount === 1) || (completionRequestCount === 5) || ((completionRequestCount % 25) === 0)) { + event('completion-requests', { comp_req_count: completionRequestCount }); + } + let parsed = docinfo.parsed; // save the typemap associated with this parsed state - we use this when resolving diff --git a/langserver/java/java-libraries.js b/langserver/java/java-libraries.js index 0d9fa17..9dca5b2 100644 --- a/langserver/java/java-libraries.js +++ b/langserver/java/java-libraries.js @@ -1,6 +1,7 @@ const fs = require('fs'); const path = require('path'); const { CEIType, loadJavaLibraryCacheFile } = require('java-mti'); +const analytics = require('../analytics'); const { trace, time, timeEnd } = require('../logging'); /** @@ -9,6 +10,7 @@ const { trace, time, timeEnd } = require('../logging'); * @returns {Promise>} */ async function loadAndroidSystemLibrary(extensionPath, additional_libs) { + analytics.time('android-library-load'); time('android-library-load'); let library; try { @@ -25,6 +27,7 @@ async function loadAndroidSystemLibrary(extensionPath, additional_libs) { library = typemap; } finally { timeEnd('android-library-load'); + analytics.timeEnd('android-library-load', 'ms', { libs: additional_libs, typecount: library ? library.size : 0 }); } return library; } diff --git a/langserver/method-signatures.js b/langserver/method-signatures.js index 2e888c5..07663db 100644 --- a/langserver/method-signatures.js +++ b/langserver/method-signatures.js @@ -2,6 +2,9 @@ const { Method } = require('java-mti'); const { indexAt } = require('./document'); const { formatDoc } = require('./doc-formatter'); const { trace } = require('./logging'); +const { event } = require('./analytics'); + +let methodsigRequestCount = 0; /** * Retrieve method signature information @@ -36,6 +39,11 @@ async function getSignatureHelp(request, liveParsers) { // wait for any active edits to complete await docinfo.reparseWaiter; + methodsigRequestCount += 1; + if ((methodsigRequestCount === 1) || (methodsigRequestCount === 5) || ((methodsigRequestCount % 25) === 0)) { + event('method-sig-requests', { methsig_req_count: methodsigRequestCount }); + } + // locate the token at the requested position const index = indexAt(request.position, docinfo.content); const token = docinfo.parsed.unit.getTokenAt(index); diff --git a/langserver/package-lock.json b/langserver/package-lock.json index 6445af2..8ebebee 100644 --- a/langserver/package-lock.json +++ b/langserver/package-lock.json @@ -301,6 +301,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz", + "integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==" + }, "vscode-jsonrpc": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz", diff --git a/langserver/package.json b/langserver/package.json index 2e1903e..4ae5083 100644 --- a/langserver/package.json +++ b/langserver/package.json @@ -11,6 +11,7 @@ "dependencies": { "java-mti": "adelphes/java-mti#d0e1e45", "mixpanel": "^0.11.0", + "uuid": "^8.2.0", "vscode-languageserver": "6.1.1", "vscode-languageserver-textdocument": "1.0.1", "vscode-uri": "2.1.2" diff --git a/langserver/server.js b/langserver/server.js index da65392..b9ee522 100644 --- a/langserver/server.js +++ b/langserver/server.js @@ -19,6 +19,10 @@ const { clearDefaultCompletionEntries, getCompletionItems, resolveCompletionItem const { getSignatureHelp } = require('./method-signatures'); const { FileURIMap, JavaDocInfo, indexAt, reparse } = require('./document'); +const { v4: uuidv4 } = require('uuid'); +const analytics = require('./analytics'); +const package_json = require('./package.json'); + /** * The global map of Android system types * @typedef {Map} AndroidLibrary @@ -131,6 +135,7 @@ connection.onInitialize((params) => { } Settings.set(startupOpts.initialSettings); + analytics.init(undefined, startupOpts.mpuid, uuidv4(), package_json, { vscode_version: startupOpts.vscodeVersion }); loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries); @@ -215,6 +220,14 @@ connection.onDidChangeConfiguration(async (change) => { Settings.set(newSettings); + if (Settings.updateCount > 2) { + analytics.event('ls-settings-changed', { + appSourceRoot: Settings.appSourceRoot, + libs: Settings.codeCompletionLibraries, + trace: Settings.trace, + }) + } + const new_ccl = [...new Set(Settings.codeCompletionLibraries)].sort(); if (new_ccl.length !== prev_ccl.length || new_ccl.find((lib,idx) => lib !== prev_ccl[idx])) { // code-completion libraries have changed - reload the android library