Revert "PERF: Move highlightjs to a background worker, and add result cache (#10191)"

This caused a CORS error when used with S3 asset storage

This reverts commit d09f283e91.
This commit is contained in:
David Taylor 2020-07-15 13:52:35 +01:00
parent c802c7367a
commit 7d300006a1
No known key found for this signature in database
GPG Key ID: 46904C18B1D3F434
11 changed files with 46 additions and 280 deletions

View File

@ -1,37 +1,11 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { highlightText } from "discourse/lib/highlight-syntax"; import { on, observes } from "discourse-common/utils/decorators";
import { escapeExpression } from "discourse/lib/utilities"; import highlightSyntax from "discourse/lib/highlight-syntax";
import discourseComputed from "discourse-common/utils/decorators";
import { htmlSafe } from "@ember/template";
export default Component.extend({ export default Component.extend({
didReceiveAttrs() { @on("didInsertElement")
this._super(...arguments); @observes("code")
if (this.code === this.previousCode) return; _refresh: function() {
highlightSyntax($(this.element));
this.set("previousCode", this.code);
this.set("highlightResult", null);
const toHighlight = this.code;
highlightText(escapeExpression(toHighlight), this.lang).then(
({ result }) => {
if (toHighlight !== this.code) return; // Code has changed since highlight was requested
this.set("highlightResult", result);
}
);
},
@discourseComputed("code", "highlightResult")
displayCode(code, highlightResult) {
if (highlightResult) return htmlSafe(highlightResult);
return code;
},
@discourseComputed("highlightResult", "lang")
codeClasses(highlightResult, lang) {
const classes = [];
if (lang) classes.push(lang);
if (highlightResult) classes.push("hljs");
return classes.join(" ");
} }
}); });

View File

@ -7,8 +7,8 @@ import discourseComputed from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { escapeExpression } from "discourse/lib/utilities"; import { escapeExpression } from "discourse/lib/utilities";
import { url } from "discourse/lib/computed";
import highlightSyntax from "discourse/lib/highlight-syntax"; import highlightSyntax from "discourse/lib/highlight-syntax";
import { url } from "discourse/lib/computed";
const THEME_UPLOAD_VAR = 2; const THEME_UPLOAD_VAR = 2;
const FIELDS_IDS = [0, 1, 5]; const FIELDS_IDS = [0, 1, 5];
@ -321,7 +321,7 @@ const Theme = RestModel.extend({
} }
} }
); );
highlightSyntax(document.querySelector(".bootbox.modal")); highlightSyntax();
} else { } else {
return this.save({ remote_update: true }).then(() => return this.save({ remote_update: true }).then(() =>
this.set("changed", false) this.set("changed", false)

View File

@ -1 +1 @@
<pre><code class={{codeClasses}}>{{displayCode}}</code></pre> <pre><code class={{lang}}>{{code}}</code></pre>

View File

@ -1,15 +1,15 @@
import highlightSyntax from "discourse/lib/highlight-syntax";
import lightbox from "discourse/lib/lightbox"; import lightbox from "discourse/lib/lightbox";
import { setupLazyLoading } from "discourse/lib/lazy-load-images"; import { setupLazyLoading } from "discourse/lib/lazy-load-images";
import { setTextDirections } from "discourse/lib/text-direction"; import { setTextDirections } from "discourse/lib/text-direction";
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
import highlightSyntax from "discourse/lib/highlight-syntax";
export default { export default {
name: "post-decorations", name: "post-decorations",
initialize(container) { initialize(container) {
withPluginApi("0.1", api => { withPluginApi("0.1", api => {
const siteSettings = container.lookup("site-settings:main"); const siteSettings = container.lookup("site-settings:main");
api.decorateCookedElement(highlightSyntax, { api.decorateCooked(highlightSyntax, {
id: "discourse-syntax-highlighting" id: "discourse-syntax-highlighting"
}); });
api.decorateCookedElement(lightbox, { id: "discourse-lightbox" }); api.decorateCookedElement(lightbox, { id: "discourse-lightbox" });

View File

@ -1,181 +1,40 @@
import { Promise } from "rsvp"; /*global hljs:true */
import { getURLWithCDN } from "discourse-common/lib/get-url"; let _moreLanguages = [];
import { next, schedule } from "@ember/runloop";
import loadScript from "discourse/lib/load-script"; import loadScript from "discourse/lib/load-script";
import { isTesting } from "discourse-common/config/environment";
let highlightJsUrl; export default function highlightSyntax($elem) {
let highlightJsWorkerUrl; const selector = Discourse.SiteSettings.autohighlight_all_code
? "pre code"
: "pre code[class]",
path = Discourse.HighlightJSPath;
const _moreLanguages = []; if (!path) {
let _worker = null; return;
let _workerPromise = null; }
const _pendingResolution = {};
let _counter = 0;
let _cachedResultsMap = new Map();
const CACHE_SIZE = 100; $(selector, $elem).each(function(i, e) {
// Large code blocks can cause crashes or slowdowns
if (e.innerHTML.length > 30000) {
return;
}
export function setupHighlightJs(args) { $(e).removeClass("lang-auto");
highlightJsUrl = args.highlightJsUrl; loadScript(path).then(() => {
highlightJsWorkerUrl = args.highlightJsWorkerUrl; customHighlightJSLanguages();
hljs.highlightBlock(e);
});
});
} }
export function registerHighlightJSLanguage(name, fn) { export function registerHighlightJSLanguage(name, fn) {
_moreLanguages.push({ name: name, fn: fn }); _moreLanguages.push({ name: name, fn: fn });
} }
export default function highlightSyntax(elem, { autoHighlight = false } = {}) { function customHighlightJSLanguages() {
const selector = autoHighlight ? "pre code" : "pre code[class]"; _moreLanguages.forEach(l => {
if (hljs.getLanguage(l.name) === undefined) {
elem.querySelectorAll(selector).forEach(e => highlightElement(e)); hljs.registerLanguage(l.name, l.fn);
}
function highlightElement(e) {
e.classList.remove("lang-auto");
let lang = null;
e.classList.forEach(c => {
if (c.startsWith("lang-")) {
lang = c.slice("lang-".length);
}
});
const requestString = e.textContent;
highlightText(e.textContent, lang).then(({ result, fromCache }) => {
const doRender = () => {
// Ensure the code hasn't changed since highlighting was triggered:
if (requestString !== e.textContent) return;
e.innerHTML = result;
e.classList.add("hljs");
};
if (fromCache) {
// This happened synchronously, we can safely add rendering
// to the end of the current Runloop
schedule("afterRender", null, doRender);
} else {
// This happened async, we are probably not in a runloop
// If we call `schedule`, a new runloop will be triggered immediately
// So schedule rendering to happen in the next runloop
next(() => schedule("afterRender", null, doRender));
} }
}); });
} }
export function highlightText(text, language) {
// Large code blocks can cause crashes or slowdowns
if (text.length > 30000) {
return Promise.resolve({ result: text, fromCache: true });
}
return getWorker().then(w => {
let result;
if ((result = _cachedResultsMap.get(cacheKey(text, language)))) {
return Promise.resolve({ result, fromCache: true });
}
let resolve;
const promise = new Promise(f => (resolve = f));
w.postMessage({
type: "highlight",
id: _counter,
text,
language
});
_pendingResolution[_counter] = {
promise,
resolve,
text,
language
};
_counter++;
return promise;
});
}
function getWorker() {
if (_worker) return Promise.resolve(_worker);
if (_workerPromise) return _workerPromise;
const w = new Worker(highlightJsWorkerUrl);
w.onmessage = onWorkerMessage;
w.postMessage({
type: "loadHighlightJs",
path: fullHighlightJsUrl()
});
_workerPromise = setupCustomLanguages(w).then(() => (_worker = w));
return _workerPromise;
}
function setupCustomLanguages(worker) {
if (_moreLanguages.length === 0) return Promise.resolve();
// To build custom language definitions we need to have hljs loaded
// Plugins/themes can't run code in a worker, so we have to load hljs in the main thread
// But the actual highlighting will still be done in the worker
return loadScript(highlightJsUrl).then(() => {
_moreLanguages.forEach(({ name, fn }) => {
const definition = fn(window.hljs);
worker.postMessage({
type: "registerLanguage",
definition,
name
});
});
});
}
function onWorkerMessage(message) {
const id = message.data.id;
const request = _pendingResolution[id];
delete _pendingResolution[id];
request.resolve({ result: message.data.result, fromCache: false });
cacheResult({
text: request.text,
language: request.language,
result: message.data.result
});
}
function cacheResult({ text, language, result }) {
_cachedResultsMap.set(cacheKey(text, language), result);
while (_cachedResultsMap.size > CACHE_SIZE) {
_cachedResultsMap.delete(_cachedResultsMap.entries().next().value[0]);
}
}
function cacheKey(text, lang) {
return `${lang}:${text}`;
}
function fullHighlightJsUrl() {
let hljsUrl = getURLWithCDN(highlightJsUrl);
// Need to use full URL including protocol/domain
// for use in a worker
if (hljsUrl.startsWith("/")) {
hljsUrl = window.location.protocol + "//" + window.location.host + hljsUrl;
}
return hljsUrl;
}
// To be used in qunit tests. Running highlight in a worker means that the
// normal system which waits for ember rendering in tests doesn't work.
// This promise will resolve once all pending highlights are done
export function waitForHighlighting() {
if (!isTesting()) {
throw "This function should only be called in a test environment";
}
const promises = Object.values(_pendingResolution).map(r => r.promise);
return new Promise(resolve => {
Promise.all(promises).then(() => next(resolve));
});
}

View File

@ -9,7 +9,6 @@ import {
} from "discourse-common/config/environment"; } from "discourse-common/config/environment";
import { setupURL, setupS3CDN } from "discourse-common/lib/get-url"; import { setupURL, setupS3CDN } from "discourse-common/lib/get-url";
import deprecated from "discourse-common/lib/deprecated"; import deprecated from "discourse-common/lib/deprecated";
import { setupHighlightJs } from "discourse/lib/highlight-syntax";
export default { export default {
name: "discourse-bootstrap", name: "discourse-bootstrap",
@ -82,11 +81,7 @@ export default {
Session.currentProp("safe_mode", setupData.safeMode); Session.currentProp("safe_mode", setupData.safeMode);
} }
setupHighlightJs({ app.HighlightJSPath = setupData.highlightJsPath;
highlightJsUrl: setupData.highlightJsUrl,
highlightJsWorkerUrl: setupData.highlightJsWorkerUrl
});
app.SvgSpritePath = setupData.svgSpritePath; app.SvgSpritePath = setupData.svgSpritePath;
if (app.Environment === "development") { if (app.Environment === "development") {

View File

@ -1,49 +0,0 @@
// discourse-skip-module
// Standalone worker for highlightjs syntax generation
// The highlightjs path changes based on site settings,
// so we wait for Discourse to pass the path into the worker
const loadHighlightJs = path => {
self.importScripts(path);
};
const highlight = ({ id, text, language }) => {
if (!self.hljs) {
throw "HighlightJS is not loaded";
}
const result = language
? self.hljs.highlight(language, text, true).value
: self.hljs.highlightAuto(text).value;
postMessage({
type: "highlightResult",
id: id,
result: result
});
};
const registerLanguage = ({ name, definition }) => {
if (!self.hljs) {
throw "HighlightJS is not loaded";
}
self.hljs.registerLanguage(name, () => {
return definition;
});
};
onmessage = event => {
const data = event.data;
const messageType = data.type;
if (messageType === "loadHighlightJs") {
loadHighlightJs(data.path);
} else if (messageType === "registerLanguage") {
registerLanguage(data);
} else if (messageType === "highlight") {
highlight(data);
} else {
throw `Unknown message type: ${messageType}`;
}
};

View File

@ -469,8 +469,7 @@ module ApplicationHelper
default_locale: SiteSetting.default_locale, default_locale: SiteSetting.default_locale,
asset_version: Discourse.assets_digest, asset_version: Discourse.assets_digest,
disable_custom_css: loading_admin?, disable_custom_css: loading_admin?,
highlight_js_url: HighlightJs.path, highlight_js_path: HighlightJs.path,
highlight_js_worker_url: script_asset_path('highlightjs-worker'),
svg_sprite_path: SvgSprite.path(theme_ids), svg_sprite_path: SvgSprite.path(theme_ids),
enable_js_error_reporting: GlobalSetting.enable_js_error_reporting, enable_js_error_reporting: GlobalSetting.enable_js_error_reporting,
} }

View File

@ -171,7 +171,6 @@ module Discourse
confirm-new-email/bootstrap.js confirm-new-email/bootstrap.js
onpopstate-handler.js onpopstate-handler.js
embed-application.js embed-application.js
highlightjs-worker.js
} }
# Precompile all available locales # Precompile all available locales

View File

@ -56,7 +56,6 @@ class DiscourseJsProcessor
activate-account activate-account
auto-redirect auto-redirect
embed-application embed-application
highlightjs-worker
app-boot app-boot
).any? { |f| relative_path == "#{js_root}/#{f}.js" } ).any? { |f| relative_path == "#{js_root}/#{f}.js" }

View File

@ -1,8 +1,4 @@
import componentTest from "helpers/component-test"; import componentTest from "helpers/component-test";
import {
waitForHighlighting,
setupHighlightJs
} from "discourse/lib/highlight-syntax";
const LONG_CODE_BLOCK = "puts a\n".repeat(15000); const LONG_CODE_BLOCK = "puts a\n".repeat(15000);
@ -12,15 +8,12 @@ componentTest("highlighting code", {
template: "{{highlighted-code lang='ruby' code=code}}", template: "{{highlighted-code lang='ruby' code=code}}",
beforeEach() { beforeEach() {
setupHighlightJs({ Discourse.HighlightJSPath =
highlightJsUrl: "/assets/highlightjs/highlight-test-bundle.min.js", "assets/highlightjs/highlight-test-bundle.min.js";
highlightJsWorkerUrl: "/assets/highlightjs-worker.js" this.set("code", "def test; end");
});
}, },
async test(assert) { async test(assert) {
this.set("code", "def test; end");
await waitForHighlighting();
assert.equal( assert.equal(
find("code.ruby.hljs .hljs-function .hljs-keyword") find("code.ruby.hljs .hljs-function .hljs-keyword")
.text() .text()
@ -30,19 +23,16 @@ componentTest("highlighting code", {
} }
}); });
componentTest("highlighting code limit", { componentTest("large code blocks are not highlighted", {
template: "{{highlighted-code lang='ruby' code=code}}", template: "{{highlighted-code lang='ruby' code=code}}",
beforeEach() { beforeEach() {
setupHighlightJs({ Discourse.HighlightJSPath =
highlightJsUrl: "/assets/highlightjs/highlight-test-bundle.min.js", "assets/highlightjs/highlight-test-bundle.min.js";
highlightJsWorkerUrl: "/assets/highlightjs-worker.js" this.set("code", LONG_CODE_BLOCK);
});
}, },
async test(assert) { async test(assert) {
this.set("code", LONG_CODE_BLOCK);
await waitForHighlighting();
assert.equal( assert.equal(
find("code") find("code")
.text() .text()