From ce52bfc59c6700e64dbe941485d078ceb9dd2158 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Tue, 18 Jan 2022 06:58:50 -0500 Subject: [PATCH] Add LSP benchmark mimicking the one on quick-lint-js (#13365) --- .dprint.json | 1 + Cargo.lock | 1 + cli/Cargo.toml | 6 + cli/bench/lsp_bench_standalone.rs | 94 ++++ cli/bench/testdata/express-router.js | 663 +++++++++++++++++++++++++++ tools/lint.js | 1 + 6 files changed, 766 insertions(+) create mode 100644 cli/bench/lsp_bench_standalone.rs create mode 100644 cli/bench/testdata/express-router.js diff --git a/.dprint.json b/.dprint.json index f870ab7727..7a88cdff34 100644 --- a/.dprint.json +++ b/.dprint.json @@ -15,6 +15,7 @@ "excludes": [ ".cargo_home", ".git", + "cli/bench/testdata/express-router.js", "cli/dts/lib.d.ts", "cli/dts/lib.dom*", "cli/dts/lib.es*", diff --git a/Cargo.lock b/Cargo.lock index 02537e5753..7c3f223386 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,6 +711,7 @@ dependencies = [ "clap_complete_fig", "data-url", "deno_ast", + "deno_bench_util", "deno_broadcast_channel", "deno_console", "deno_core", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 824cb4da0b..1e5c1781e7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,6 +19,11 @@ name = "deno_bench" harness = false path = "./bench/main.rs" +[[bench]] +name = "lsp_bench_standalone" +harness = false +path = "./bench/lsp_bench_standalone.rs" + [build-dependencies] deno_broadcast_channel = { version = "0.26.0", path = "../ext/broadcast_channel" } deno_console = { version = "0.32.0", path = "../ext/console" } @@ -96,6 +101,7 @@ fwdansi = "=1.1.0" winapi = { version = "=0.3.9", features = ["knownfolders", "mswsock", "objbase", "shlobj", "tlhelp32", "winbase", "winerror", "winsock2"] } [dev-dependencies] +deno_bench_util = { version = "0.26.0", path = "../bench_util" } flaky_test = "=0.1.0" os_pipe = "=0.9.2" pretty_assertions = "=0.7.2" diff --git a/cli/bench/lsp_bench_standalone.rs b/cli/bench/lsp_bench_standalone.rs new file mode 100644 index 0000000000..4c18a9a9d5 --- /dev/null +++ b/cli/bench/lsp_bench_standalone.rs @@ -0,0 +1,94 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use deno_bench_util::bencher::benchmark_group; +use deno_bench_util::bencher::benchmark_main; +use deno_bench_util::bencher::Bencher; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use test_util::lsp::LspClient; + +// Intended to match the benchmark in quick-lint-js +// https://github.com/quick-lint/quick-lint-js/blob/35207e6616267c6c81be63f47ce97ec2452d60df/benchmark/benchmark-lsp/lsp-benchmarks.cpp#L223-L268 +fn incremental_change_wait(bench: &mut Bencher) { + let deno_exe = test_util::deno_exe_path(); + let mut client = LspClient::new(&deno_exe).unwrap(); + + static FIXTURE_INIT_JSON: &[u8] = + include_bytes!("testdata/initialize_params.json"); + let params: Value = serde_json::from_slice(FIXTURE_INIT_JSON).unwrap(); + let (_, maybe_err) = client + .write_request::<_, _, Value>("initialize", params) + .unwrap(); + assert!(maybe_err.is_none()); + client.write_notification("initialized", json!({})).unwrap(); + + client + .write_notification( + "textDocument/didOpen", + json!({ + "textDocument": { + "uri": "file:///testdata/express-router.js", + "languageId": "javascript", + "version": 0, + "text": include_str!("testdata/express-router.js") + } + }), + ) + .unwrap(); + let (method, _maybe_diag): (String, Option) = + client.read_notification().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + + let mut document_version: u64 = 0; + bench.iter(|| { + let text = format!("m{:05}", document_version); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "version": document_version, + "uri":"file:///testdata/express-router.js" + }, + "contentChanges": [ + {"text": text, "range":{"start":{"line":506,"character":39},"end":{"line":506,"character":45}}}, + {"text": text, "range":{"start":{"line":507,"character":8},"end":{"line":507,"character":14}}}, + {"text": text, "range":{"start":{"line":509,"character":10},"end":{"line":509,"character":16}}} + ] + }) + ).unwrap(); + + wait_for_deno_lint_diagnostic(document_version, &mut client); + + document_version += 1; + }) +} + +fn wait_for_deno_lint_diagnostic( + document_version: u64, + client: &mut LspClient, +) { + loop { + let (method, maybe_diag): (String, Option) = + client.read_notification().unwrap(); + if method == "textDocument/publishDiagnostics" { + let d = maybe_diag.unwrap(); + let msg = d.as_object().unwrap(); + let version = msg.get("version").unwrap().as_u64().unwrap(); + if document_version == version { + let diagnostics = msg.get("diagnostics").unwrap().as_array().unwrap(); + let first = &diagnostics[0]; + let source = first.get("source").unwrap().as_str().unwrap(); + if source == "deno-lint" { + return; + } + } + } else { + todo!() // handle_misc_message + } + } +} + +benchmark_group!(benches, incremental_change_wait); +benchmark_main!(benches); diff --git a/cli/bench/testdata/express-router.js b/cli/bench/testdata/express-router.js new file mode 100644 index 0000000000..9b1365aa15 --- /dev/null +++ b/cli/bench/testdata/express-router.js @@ -0,0 +1,663 @@ +/*! + * express + * Copyright(c) 2009-2013 TJ Holowaychuk + * Copyright(c) 2013 Roman Shtylman + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + +var Route = require('./route'); +var Layer = require('./layer'); +var methods = require('methods'); +var mixin = require('utils-merge'); +var debug = require('debug')('express:router'); +var deprecate = require('depd')('express'); +var flatten = require('array-flatten'); +var parseUrl = require('parseurl'); +var setPrototypeOf = require('setprototypeof') + +/** + * Module variables. + * @private + */ + +var objectRegExp = /^\[object (\S+)\]$/; +var slice = Array.prototype.slice; +var toString = Object.prototype.toString; + +/** + * Initialize a new `Router` with the given `options`. + * + * @param {Object} [options] + * @return {Router} which is an callable function + * @public + */ + +var proto = module.exports = function(options) { + var opts = options || {}; + + function router(req, res, next) { + router.handle(req, res, next); + } + + // mixin Router class functions + setPrototypeOf(router, proto) + + router.params = {}; + router._params = []; + router.caseSensitive = opts.caseSensitive; + router.mergeParams = opts.mergeParams; + router.strict = opts.strict; + router.stack = []; + + return router; +}; + +/** + * Map the given param placeholder `name`(s) to the given callback. + * + * Parameter mapping is used to provide pre-conditions to routes + * which use normalized placeholders. For example a _:user_id_ parameter + * could automatically load a user's information from the database without + * any additional code, + * + * The callback uses the same signature as middleware, the only difference + * being that the value of the placeholder is passed, in this case the _id_ + * of the user. Once the `next()` function is invoked, just like middleware + * it will continue on to execute the route, or subsequent parameter functions. + * + * Just like in middleware, you must either respond to the request or call next + * to avoid stalling the request. + * + * app.param('user_id', function(req, res, next, id){ + * User.find(id, function(err, user){ + * if (err) { + * return next(err); + * } else if (!user) { + * return next(new Error('failed to load user')); + * } + * req.user = user; + * next(); + * }); + * }); + * + * @param {String} name + * @param {Function} fn + * @return {app} for chaining + * @public + */ + +proto.param = function param(name, fn) { + // param logic + if (typeof name === 'function') { + deprecate('router.param(fn): Refactor to use path params'); + this._params.push(name); + return; + } + + // apply param functions + var params = this._params; + var len = params.length; + var ret; + + if (name[0] === ':') { + deprecate('router.param(' + JSON.stringify(name) + ', fn): Use router.param(' + JSON.stringify(name.substr(1)) + ', fn) instead'); + name = name.substr(1); + } + + for (var i = 0; i < len; ++i) { + if (ret = params[i](name, fn)) { + fn = ret; + } + } + + // ensure we end up with a + // middleware function + if ('function' !== typeof fn) { + throw new Error('invalid param() call for ' + name + ', got ' + fn); + } + + (this.params[name] = this.params[name] || []).push(fn); + return this; +}; + +/** + * Dispatch a req, res into the router. + * @private + */ + +proto.handle = function handle(req, res, out) { + var self = this; + + debug('dispatching %s %s', req.method, req.url); + + var idx = 0; + var protohost = getProtohost(req.url) || '' + var removed = ''; + var slashAdded = false; + var paramcalled = {}; + + // store options for OPTIONS request + // only used if OPTIONS request + var options = []; + + // middleware and routes + var stack = self.stack; + + // manage inter-router variables + var parentParams = req.params; + var parentUrl = req.baseUrl || ''; + var done = restore(out, req, 'baseUrl', 'next', 'params'); + + // setup next layer + req.next = next; + + // for options requests, respond with a default if nothing else responds + if (req.method === 'OPTIONS') { + done = wrap(done, function(old, err) { + if (err || options.length === 0) return old(err); + sendOptionsResponse(res, options, old); + }); + } + + // setup basic req values + req.baseUrl = parentUrl; + req.originalUrl = req.originalUrl || req.url; + + next(); + + function next(err) { + var layerError = err === 'route' + ? null + : err; + + // remove added slash + if (slashAdded) { + req.url = req.url.substr(1); + slashAdded = false; + } + + // restore altered req.url + if (removed.length !== 0) { + req.baseUrl = parentUrl; + req.url = protohost + removed + req.url.substr(protohost.length); + removed = ''; + } + + // signal to exit router + if (layerError === 'router') { + setImmediate(done, null) + return + } + + // no more matching layers + if (idx >= stack.length) { + setImmediate(done, layerError); + return; + } + + // get pathname of request + var path = getPathname(req); + + if (path == null) { + return done(layerError); + } + + // find next matching layer + var layer; + var match; + var route; + + while (match !== true && idx < stack.length) { + layer = stack[idx++]; + match = matchLayer(layer, path); + route = layer.route; + + if (typeof match !== 'boolean') { + // hold on to layerError + layerError = layerError || match; + } + + if (match !== true) { + continue; + } + + if (!route) { + // process non-route handlers normally + continue; + } + + if (layerError) { + // routes do not match with a pending error + match = false; + continue; + } + + var method = req.method; + var has_method = route._handles_method(method); + + // build up automatic options response + if (!has_method && method === 'OPTIONS') { + appendMethods(options, route._options()); + } + + // don't even bother matching route + if (!has_method && method !== 'HEAD') { + match = false; + continue; + } + } + + // no match + if (match !== true) { + return done(layerError); + } + + // store route for dispatch on change + if (route) { + req.route = route; + } + + // Capture one-time layer values + req.params = self.mergeParams + ? mergeParams(layer.params, parentParams) + : layer.params; + var layerPath = layer.path; + + // this should be done for the layer + self.process_params(layer, paramcalled, req, res, function (err) { + if (err) { + return next(layerError || err); + } + + if (route) { + return layer.handle_request(req, res, next); + } + + trim_prefix(layer, layerError, layerPath, path); + }); + } + + function trim_prefix(layer, layerError, layerPath, path) { + if (layerPath.length !== 0) { + // Validate path breaks on a path separator + var c = path[layerPath.length] + if (c && c !== '/' && c !== '.') return next(layerError) + + // Trim off the part of the url that matches the route + // middleware (.use stuff) needs to have the path stripped + debug('trim prefix (%s) from url %s', layerPath, req.url); + removed = layerPath; + req.url = protohost + req.url.substr(protohost.length + removed.length); + + // Ensure leading slash + if (!protohost && req.url[0] !== '/') { + req.url = '/' + req.url; + slashAdded = true; + } + + // Setup base URL (no trailing slash) + req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' + ? removed.substring(0, removed.length - 1) + : removed); + } + + debug('%s %s : %s', layer.name, layerPath, req.originalUrl); + + if (layerError) { + layer.handle_error(layerError, req, res, next); + } else { + layer.handle_request(req, res, next); + } + } +}; + +/** + * Process any parameters for the layer. + * @private + */ + +proto.process_params = function process_params(layer, called, req, res, done) { + var params = this.params; + + // captured parameters from the layer, keys and values + var keys = layer.keys; + + // fast track + if (!keys || keys.length === 0) { + return done(); + } + + var i = 0; + var name; + var paramIndex = 0; + var key; + var paramVal; + var paramCallbacks; + var paramCalled; + + // process params in order + // param callbacks can be async + function param(err) { + if (err) { + return done(err); + } + + if (i >= keys.length ) { + return done(); + } + + paramIndex = 0; + key = keys[i++]; + name = key.name; + paramVal = req.params[name]; + paramCallbacks = params[name]; + paramCalled = called[name]; + + if (paramVal === undefined || !paramCallbacks) { + return param(); + } + + // param previously called with same value or error occurred + if (paramCalled && (paramCalled.match === paramVal + || (paramCalled.error && paramCalled.error !== 'route'))) { + // restore value + req.params[name] = paramCalled.value; + + // next param + return param(paramCalled.error); + } + + called[name] = paramCalled = { + error: null, + match: paramVal, + value: paramVal + }; + + paramCallback(); + } + + // single param callbacks + function paramCallback(err) { + var fn = paramCallbacks[paramIndex++]; + + // store updated value + paramCalled.value = req.params[key.name]; + + if (err) { + // store error + paramCalled.error = err; + param(err); + return; + } + + if (!fn) return param(); + + try { + fn(req, res, paramCallback, paramVal, key.name); + } catch (e) { + paramCallback(e); + } + } + + param(); +}; + +/** + * Use the given middleware function, with optional path, defaulting to "/". + * + * Use (like `.all`) will run for any http METHOD, but it will not add + * handlers for those methods so OPTIONS requests will not consider `.use` + * functions even if they could respond. + * + * The other difference is that _route_ path is stripped and not visible + * to the handler function. The main effect of this feature is that mounted + * handlers can operate without any code changes regardless of the "prefix" + * pathname. + * + * @public + */ + +proto.use = function use(fn) { + var offset = 0; + var path = '/'; + + // default path to '/' + // disambiguate router.use([fn]) + if (typeof fn !== 'function') { + var arg = fn; + + while (Array.isArray(arg) && arg.length !== 0) { + arg = arg[0]; + } + + // first arg is the path + if (typeof arg !== 'function') { + offset = 1; + path = fn; + } + } + + var callbacks = flatten(slice.call(arguments, offset)); + + if (callbacks.length === 0) { + throw new TypeError('Router.use() requires a middleware function') + } + + for (var i = 0; i < callbacks.length; i++) { + var fn = callbacks[i]; + + if (typeof fn !== 'function') { + throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn)) + } + + // add the middleware + debug('use %o %s', path, fn.name || '') + + var layer = new Layer(path, { + sensitive: this.caseSensitive, + strict: false, + end: false + }, fn); + + layer.route = undefined; + + this.stack.push(layer); + } + + return this; +}; + +/** + * Create a new Route for the given path. + * + * Each route contains a separate middleware stack and VERB handlers. + * + * See the Route api documentation for details on adding handlers + * and middleware to routes. + * + * @param {String} path + * @return {Route} + * @public + */ + +proto.route = function route(path) { + var route = new Route(path); + + var layer = new Layer(path, { + sensitive: this.caseSensitive, + strict: this.strict, + end: true + }, route.dispatch.bind(route)); + + layer.route = route; + + this.stack.push(layer); + return route; +}; + +// create Router#VERB functions +methods.concat('all').forEach(function(method){ + proto[method] = function(path){ + var route = this.route(path) + route[method].apply(route, slice.call(arguments, 1)); + return this; + }; +}); + +// append methods to a list of methods +function appendMethods(list, addition) { + for (var i = 0; i < addition.length; i++) { + var method = addition[i]; + if (list.indexOf(method) === -1) { + list.push(method); + } + } +} + +// get pathname of request +function getPathname(req) { + try { + return parseUrl(req).pathname; + } catch (err) { + return undefined; + } +} + +// Get get protocol + host for a URL +function getProtohost(url) { + if (typeof url !== 'string' || url.length === 0 || url[0] === '/') { + return undefined + } + + var searchIndex = url.indexOf('?') + var pathLength = searchIndex !== -1 + ? searchIndex + : url.length + var fqdnIndex = url.substr(0, pathLength).indexOf('://') + + return fqdnIndex !== -1 + ? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) + : undefined +} + +// get type for error message +function gettype(obj) { + var type = typeof obj; + + if (type !== 'object') { + return type; + } + + // inspect [[Class]] for objects + return toString.call(obj) + .replace(objectRegExp, '$1'); +} + +/** + * Match path to a layer. + * + * @param {Layer} layer + * @param {string} path + * @private + */ + +function matchLayer(layer, path) { + try { + return layer.match(path); + } catch (err) { + return err; + } +} + +// merge params with parent params +function mergeParams(params, parent) { + if (typeof parent !== 'object' || !parent) { + return params; + } + + // make copy of parent for base + var obj = mixin({}, parent); + + // simple non-numeric merging + if (!(0 in params) || !(0 in parent)) { + return mixin(obj, params); + } + + var i = 0; + var o = 0; + + // determine numeric gaps + while (i in params) { + i++; + } + + while (o in parent) { + o++; + } + + // offset numeric indices in params before merge + for (i--; i >= 0; i--) { + params[i + o] = params[i]; + + // create holes for the merge when necessary + if (i < o) { + delete params[i]; + } + } + + return mixin(obj, params); +} + +// restore obj props after function +function restore(fn, obj) { + var props = new Array(arguments.length - 2); + var vals = new Array(arguments.length - 2); + + for (var i = 0; i < props.length; i++) { + props[i] = arguments[i + 2]; + vals[i] = obj[props[i]]; + } + + return function () { + // restore vals + for (var i = 0; i < props.length; i++) { + obj[props[i]] = vals[i]; + } + + return fn.apply(this, arguments); + }; +} + +// send an OPTIONS response +function sendOptionsResponse(res, options, next) { + try { + var body = options.join(','); + res.set('Allow', body); + res.send(body); + } catch (err) { + next(err); + } +} + +// wrap a function +function wrap(old, fn) { + return function proxy() { + var args = new Array(arguments.length + 1); + + args[0] = old; + for (var i = 0, len = arguments.length; i < len; i++) { + args[i + 1] = arguments[i]; + } + + fn.apply(this, args); + }; +} +if (false) undeclaredVariable.hasOwnProperty(); // Intentional error to force diagnostics. diff --git a/tools/lint.js b/tools/lint.js index 0bbcb1fe02..a431e05c01 100755 --- a/tools/lint.js +++ b/tools/lint.js @@ -21,6 +21,7 @@ async function dlint() { ":!:cli/tests/testdata/038_checkjs.js", ":!:cli/tests/testdata/error_008_checkjs.js", ":!:cli/bench/node*.js", + ":!:cli/bench/testdata/express-router.js", ":!:cli/compilers/wasm_wrap.js", ":!:cli/dts/**", ":!:cli/tests/testdata/encoding/**",