mirror of
https://github.com/denoland/deno.git
synced 2024-11-25 15:29:32 -05:00
perf(core): Cache source lookups (#14816)
Keep a cache for source maps and source lines. We sort of already had a cache argument for source map lookup functions but we just passed an empty map instead of storing it. Extended it to cache source line lookups as well and plugged it into runtime state.
This commit is contained in:
parent
94d369ebc6
commit
79b42808a4
6 changed files with 147 additions and 152 deletions
|
@ -8,8 +8,6 @@ use deno_core::error::format_file_name;
|
||||||
use deno_core::error::JsError;
|
use deno_core::error::JsError;
|
||||||
use deno_core::error::JsStackFrame;
|
use deno_core::error::JsStackFrame;
|
||||||
|
|
||||||
const SOURCE_ABBREV_THRESHOLD: usize = 150;
|
|
||||||
|
|
||||||
// Keep in sync with `/core/error.js`.
|
// Keep in sync with `/core/error.js`.
|
||||||
pub fn format_location(frame: &JsStackFrame) -> String {
|
pub fn format_location(frame: &JsStackFrame) -> String {
|
||||||
let _internal = frame
|
let _internal = frame
|
||||||
|
@ -115,8 +113,7 @@ fn format_maybe_source_line(
|
||||||
let source_line = source_line.unwrap();
|
let source_line = source_line.unwrap();
|
||||||
// sometimes source_line gets set with an empty string, which then outputs
|
// sometimes source_line gets set with an empty string, which then outputs
|
||||||
// an empty source line when displayed, so need just short circuit here.
|
// an empty source line when displayed, so need just short circuit here.
|
||||||
// Also short-circuit on error line too long.
|
if source_line.is_empty() {
|
||||||
if source_line.is_empty() || source_line.len() > SOURCE_ABBREV_THRESHOLD {
|
|
||||||
return "".to_string();
|
return "".to_string();
|
||||||
}
|
}
|
||||||
if source_line.contains("Couldn't format source line: ") {
|
if source_line.contains("Couldn't format source line: ") {
|
||||||
|
|
|
@ -694,11 +694,9 @@ impl SourceMapGetter for ProcState {
|
||||||
_ => return None,
|
_ => return None,
|
||||||
}
|
}
|
||||||
if let Some((code, maybe_map)) = self.get_emit(&specifier) {
|
if let Some((code, maybe_map)) = self.get_emit(&specifier) {
|
||||||
let code = String::from_utf8(code).unwrap();
|
source_map_from_code(&code).or(maybe_map)
|
||||||
source_map_from_code(code).or(maybe_map)
|
|
||||||
} else if let Ok(source) = self.load(specifier, None, false) {
|
} else if let Ok(source) = self.load(specifier, None, false) {
|
||||||
let code = String::from_utf8(source.code.to_vec()).unwrap();
|
source_map_from_code(&source.code)
|
||||||
source_map_from_code(code)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -756,21 +754,14 @@ pub fn import_map_from_text(
|
||||||
Ok(result.import_map)
|
Ok(result.import_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source_map_from_code(code: String) -> Option<Vec<u8>> {
|
fn source_map_from_code(code: &[u8]) -> Option<Vec<u8>> {
|
||||||
let lines: Vec<&str> = code.split('\n').collect();
|
static PREFIX: &[u8] = b"//# sourceMappingURL=data:application/json;base64,";
|
||||||
if let Some(last_line) = lines.last() {
|
let last_line = code.rsplitn(2, |u| u == &b'\n').next().unwrap();
|
||||||
if last_line
|
if last_line.starts_with(PREFIX) {
|
||||||
.starts_with("//# sourceMappingURL=data:application/json;base64,")
|
let input = last_line.split_at(PREFIX.len()).1;
|
||||||
{
|
let decoded_map = base64::decode(input)
|
||||||
let input = last_line.trim_start_matches(
|
.expect("Unable to decode source map from emitted file.");
|
||||||
"//# sourceMappingURL=data:application/json;base64,",
|
Some(decoded_map)
|
||||||
);
|
|
||||||
let decoded_map = base64::decode(input)
|
|
||||||
.expect("Unable to decode source map from emitted file.");
|
|
||||||
Some(decoded_map)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
140
core/error.rs
140
core/error.rs
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
use crate::runtime::JsRuntime;
|
use crate::runtime::JsRuntime;
|
||||||
use crate::source_map::apply_source_map;
|
use crate::source_map::apply_source_map;
|
||||||
|
use crate::source_map::get_source_line;
|
||||||
use crate::url::Url;
|
use crate::url::Url;
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
@ -192,15 +192,20 @@ impl JsError {
|
||||||
let msg = v8::Exception::create_message(scope, exception);
|
let msg = v8::Exception::create_message(scope, exception);
|
||||||
|
|
||||||
let mut exception_message = None;
|
let mut exception_message = None;
|
||||||
let state_rc = JsRuntime::state(scope);
|
// Nest this state borrow. A mutable borrow can occur when accessing `stack`
|
||||||
let state = state_rc.borrow();
|
// in this outer scope, invoking `Error.prepareStackTrace()` which calls
|
||||||
if let Some(format_exception_cb) = &state.js_format_exception_cb {
|
// `op_apply_source_map`.
|
||||||
let format_exception_cb = format_exception_cb.open(scope);
|
{
|
||||||
let this = v8::undefined(scope).into();
|
let state_rc = JsRuntime::state(scope);
|
||||||
let formatted = format_exception_cb.call(scope, this, &[exception]);
|
let state = state_rc.borrow();
|
||||||
if let Some(formatted) = formatted {
|
if let Some(format_exception_cb) = &state.js_format_exception_cb {
|
||||||
if formatted.is_string() {
|
let format_exception_cb = format_exception_cb.open(scope);
|
||||||
exception_message = Some(formatted.to_rust_string_lossy(scope));
|
let this = v8::undefined(scope).into();
|
||||||
|
let formatted = format_exception_cb.call(scope, this, &[exception]);
|
||||||
|
if let Some(formatted) = formatted {
|
||||||
|
if formatted.is_string() {
|
||||||
|
exception_message = Some(formatted.to_rust_string_lossy(scope));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -255,66 +260,73 @@ impl JsError {
|
||||||
None => vec![],
|
None => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let state_rc = JsRuntime::state(scope);
|
|
||||||
let state = state_rc.borrow();
|
|
||||||
|
|
||||||
// When the stack frame array is empty, but the source location given by
|
|
||||||
// (script_resource_name, line_number, start_column + 1) exists, this is
|
|
||||||
// likely a syntax error. For the sake of formatting we treat it like it
|
|
||||||
// was given as a single stack frame.
|
|
||||||
if frames.is_empty() {
|
|
||||||
let script_resource_name = msg
|
|
||||||
.get_script_resource_name(scope)
|
|
||||||
.and_then(|v| v8::Local::<v8::String>::try_from(v).ok())
|
|
||||||
.map(|v| v.to_rust_string_lossy(scope));
|
|
||||||
let line_number: Option<i64> =
|
|
||||||
msg.get_line_number(scope).and_then(|v| v.try_into().ok());
|
|
||||||
let column_number: Option<i64> = msg.get_start_column().try_into().ok();
|
|
||||||
if let (Some(f), Some(l), Some(c)) =
|
|
||||||
(script_resource_name, line_number, column_number)
|
|
||||||
{
|
|
||||||
// V8's column numbers are 0-based, we want 1-based.
|
|
||||||
let c = c + 1;
|
|
||||||
if let Some(source_map_getter) = &state.source_map_getter {
|
|
||||||
let (f, l, c, _) = apply_source_map(
|
|
||||||
f,
|
|
||||||
l,
|
|
||||||
c,
|
|
||||||
&mut HashMap::new(),
|
|
||||||
source_map_getter.as_ref(),
|
|
||||||
);
|
|
||||||
frames =
|
|
||||||
vec![JsStackFrame::from_location(Some(f), Some(l), Some(c))];
|
|
||||||
} else {
|
|
||||||
frames =
|
|
||||||
vec![JsStackFrame::from_location(Some(f), Some(l), Some(c))];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut source_line = None;
|
let mut source_line = None;
|
||||||
let mut source_line_frame_index = None;
|
let mut source_line_frame_index = None;
|
||||||
if let Some(source_map_getter) = &state.source_map_getter {
|
{
|
||||||
for (i, frame) in frames.iter().enumerate() {
|
let state_rc = JsRuntime::state(scope);
|
||||||
if let (Some(file_name), Some(line_number)) =
|
let state = &mut *state_rc.borrow_mut();
|
||||||
(&frame.file_name, frame.line_number)
|
|
||||||
|
// When the stack frame array is empty, but the source location given by
|
||||||
|
// (script_resource_name, line_number, start_column + 1) exists, this is
|
||||||
|
// likely a syntax error. For the sake of formatting we treat it like it
|
||||||
|
// was given as a single stack frame.
|
||||||
|
if frames.is_empty() {
|
||||||
|
let script_resource_name = msg
|
||||||
|
.get_script_resource_name(scope)
|
||||||
|
.and_then(|v| v8::Local::<v8::String>::try_from(v).ok())
|
||||||
|
.map(|v| v.to_rust_string_lossy(scope));
|
||||||
|
let line_number: Option<i64> =
|
||||||
|
msg.get_line_number(scope).and_then(|v| v.try_into().ok());
|
||||||
|
let column_number: Option<i64> =
|
||||||
|
msg.get_start_column().try_into().ok();
|
||||||
|
if let (Some(f), Some(l), Some(c)) =
|
||||||
|
(script_resource_name, line_number, column_number)
|
||||||
{
|
{
|
||||||
if !file_name.trim_start_matches('[').starts_with("deno:") {
|
// V8's column numbers are 0-based, we want 1-based.
|
||||||
// Source lookup expects a 0-based line number, ours are 1-based.
|
let c = c + 1;
|
||||||
source_line = source_map_getter
|
if let Some(source_map_getter) = &state.source_map_getter {
|
||||||
.get_source_line(file_name, (line_number - 1) as usize);
|
let (f, l, c) = apply_source_map(
|
||||||
source_line_frame_index = Some(i);
|
f,
|
||||||
break;
|
l,
|
||||||
|
c,
|
||||||
|
&mut state.source_map_cache,
|
||||||
|
source_map_getter.as_ref(),
|
||||||
|
);
|
||||||
|
frames =
|
||||||
|
vec![JsStackFrame::from_location(Some(f), Some(l), Some(c))];
|
||||||
|
} else {
|
||||||
|
frames =
|
||||||
|
vec![JsStackFrame::from_location(Some(f), Some(l), Some(c))];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(frame) = frames.first() {
|
|
||||||
if let Some(file_name) = &frame.file_name {
|
if let Some(source_map_getter) = &state.source_map_getter {
|
||||||
if !file_name.trim_start_matches('[').starts_with("deno:") {
|
for (i, frame) in frames.iter().enumerate() {
|
||||||
source_line = msg
|
if let (Some(file_name), Some(line_number)) =
|
||||||
.get_source_line(scope)
|
(&frame.file_name, frame.line_number)
|
||||||
.map(|v| v.to_rust_string_lossy(scope));
|
{
|
||||||
source_line_frame_index = Some(0);
|
if !file_name.trim_start_matches('[').starts_with("deno:") {
|
||||||
|
// Source lookup expects a 0-based line number, ours are 1-based.
|
||||||
|
source_line = get_source_line(
|
||||||
|
file_name,
|
||||||
|
line_number,
|
||||||
|
&mut state.source_map_cache,
|
||||||
|
source_map_getter.as_ref(),
|
||||||
|
);
|
||||||
|
source_line_frame_index = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(frame) = frames.first() {
|
||||||
|
if let Some(file_name) = &frame.file_name {
|
||||||
|
if !file_name.trim_start_matches('[').starts_with("deno:") {
|
||||||
|
source_line = msg
|
||||||
|
.get_source_line(scope)
|
||||||
|
.map(|v| v.to_rust_string_lossy(scope));
|
||||||
|
source_line_frame_index = Some(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -752,14 +752,14 @@ fn op_apply_source_map(
|
||||||
location: Location,
|
location: Location,
|
||||||
) -> Result<Location, Error> {
|
) -> Result<Location, Error> {
|
||||||
let state_rc = JsRuntime::state(scope);
|
let state_rc = JsRuntime::state(scope);
|
||||||
let state = state_rc.borrow();
|
let state = &mut *state_rc.borrow_mut();
|
||||||
if let Some(source_map_getter) = &state.source_map_getter {
|
if let Some(source_map_getter) = &state.source_map_getter {
|
||||||
let mut location = location;
|
let mut location = location;
|
||||||
let (f, l, c, _) = apply_source_map_(
|
let (f, l, c) = apply_source_map_(
|
||||||
location.file_name,
|
location.file_name,
|
||||||
location.line_number.into(),
|
location.line_number.into(),
|
||||||
location.column_number.into(),
|
location.column_number.into(),
|
||||||
&mut Default::default(),
|
&mut state.source_map_cache,
|
||||||
source_map_getter.as_ref(),
|
source_map_getter.as_ref(),
|
||||||
);
|
);
|
||||||
location.file_name = f;
|
location.file_name = f;
|
||||||
|
|
|
@ -17,6 +17,7 @@ use crate::modules::NoopModuleLoader;
|
||||||
use crate::op_void_async;
|
use crate::op_void_async;
|
||||||
use crate::op_void_sync;
|
use crate::op_void_sync;
|
||||||
use crate::ops::*;
|
use crate::ops::*;
|
||||||
|
use crate::source_map::SourceMapCache;
|
||||||
use crate::source_map::SourceMapGetter;
|
use crate::source_map::SourceMapGetter;
|
||||||
use crate::Extension;
|
use crate::Extension;
|
||||||
use crate::OpMiddlewareFn;
|
use crate::OpMiddlewareFn;
|
||||||
|
@ -163,6 +164,7 @@ pub(crate) struct JsRuntimeState {
|
||||||
/// of the event loop.
|
/// of the event loop.
|
||||||
dyn_module_evaluate_idle_counter: u32,
|
dyn_module_evaluate_idle_counter: u32,
|
||||||
pub(crate) source_map_getter: Option<Box<dyn SourceMapGetter>>,
|
pub(crate) source_map_getter: Option<Box<dyn SourceMapGetter>>,
|
||||||
|
pub(crate) source_map_cache: SourceMapCache,
|
||||||
pub(crate) pending_ops: FuturesUnordered<PendingOpFuture>,
|
pub(crate) pending_ops: FuturesUnordered<PendingOpFuture>,
|
||||||
pub(crate) unrefed_ops: HashSet<i32>,
|
pub(crate) unrefed_ops: HashSet<i32>,
|
||||||
pub(crate) have_unpolled_ops: bool,
|
pub(crate) have_unpolled_ops: bool,
|
||||||
|
@ -391,6 +393,7 @@ impl JsRuntime {
|
||||||
has_tick_scheduled: false,
|
has_tick_scheduled: false,
|
||||||
js_wasm_streaming_cb: None,
|
js_wasm_streaming_cb: None,
|
||||||
source_map_getter: options.source_map_getter,
|
source_map_getter: options.source_map_getter,
|
||||||
|
source_map_cache: Default::default(),
|
||||||
pending_ops: FuturesUnordered::new(),
|
pending_ops: FuturesUnordered::new(),
|
||||||
unrefed_ops: HashSet::new(),
|
unrefed_ops: HashSet::new(),
|
||||||
shared_array_buffer_store: options.shared_array_buffer_store,
|
shared_array_buffer_store: options.shared_array_buffer_store,
|
||||||
|
|
|
@ -17,83 +17,75 @@ pub trait SourceMapGetter {
|
||||||
) -> Option<String>;
|
) -> Option<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cached filename lookups. The key can be None if a previous lookup failed to
|
#[derive(Debug, Default)]
|
||||||
/// find a SourceMap.
|
pub struct SourceMapCache {
|
||||||
pub type CachedMaps = HashMap<String, Option<SourceMap>>;
|
maps: HashMap<String, Option<SourceMap>>,
|
||||||
|
source_lines: HashMap<(String, i64), Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_source_map<G: SourceMapGetter + ?Sized>(
|
pub fn apply_source_map<G: SourceMapGetter + ?Sized>(
|
||||||
file_name: String,
|
file_name: String,
|
||||||
line_number: i64,
|
line_number: i64,
|
||||||
column_number: i64,
|
column_number: i64,
|
||||||
mappings_map: &mut CachedMaps,
|
cache: &mut SourceMapCache,
|
||||||
getter: &G,
|
getter: &G,
|
||||||
) -> (String, i64, i64, Option<String>) {
|
) -> (String, i64, i64) {
|
||||||
// Lookup expects 0-based line and column numbers, but ours are 1-based.
|
// Lookup expects 0-based line and column numbers, but ours are 1-based.
|
||||||
let line_number = line_number - 1;
|
let line_number = line_number - 1;
|
||||||
let column_number = column_number - 1;
|
let column_number = column_number - 1;
|
||||||
|
|
||||||
let default_pos = (file_name.clone(), line_number, column_number, None);
|
let default_pos = (file_name.clone(), line_number, column_number);
|
||||||
let maybe_source_map = get_mappings(&file_name, mappings_map, getter);
|
let maybe_source_map =
|
||||||
let (file_name, line_number, column_number, source_line) =
|
cache.maps.entry(file_name.clone()).or_insert_with(|| {
|
||||||
match maybe_source_map {
|
getter
|
||||||
None => default_pos,
|
.get_source_map(&file_name)
|
||||||
Some(source_map) => {
|
.and_then(|raw_source_map| SourceMap::from_slice(&raw_source_map).ok())
|
||||||
match source_map.lookup_token(line_number as u32, column_number as u32)
|
});
|
||||||
{
|
let (file_name, line_number, column_number) = match maybe_source_map {
|
||||||
|
None => default_pos,
|
||||||
|
Some(source_map) => {
|
||||||
|
match source_map.lookup_token(line_number as u32, column_number as u32) {
|
||||||
|
None => default_pos,
|
||||||
|
Some(token) => match token.get_source() {
|
||||||
None => default_pos,
|
None => default_pos,
|
||||||
Some(token) => match token.get_source() {
|
Some(source_file_name) => {
|
||||||
None => default_pos,
|
// The `source_file_name` written by tsc in the source map is
|
||||||
Some(source_file_name) => {
|
// sometimes only the basename of the URL, or has unwanted `<`/`>`
|
||||||
// The `source_file_name` written by tsc in the source map is
|
// around it. Use the `file_name` we get from V8 if
|
||||||
// sometimes only the basename of the URL, or has unwanted `<`/`>`
|
// `source_file_name` does not parse as a URL.
|
||||||
// around it. Use the `file_name` we get from V8 if
|
let file_name = match resolve_url(source_file_name) {
|
||||||
// `source_file_name` does not parse as a URL.
|
Ok(m) if m.scheme() == "blob" => file_name,
|
||||||
let file_name = match resolve_url(source_file_name) {
|
Ok(m) => m.to_string(),
|
||||||
Ok(m) if m.scheme() == "blob" => file_name,
|
Err(_) => file_name,
|
||||||
Ok(m) => m.to_string(),
|
};
|
||||||
Err(_) => file_name,
|
(
|
||||||
};
|
file_name,
|
||||||
let source_line =
|
i64::from(token.get_src_line()),
|
||||||
if let Some(source_view) = token.get_source_view() {
|
i64::from(token.get_src_col()),
|
||||||
source_view
|
)
|
||||||
.get_line(token.get_src_line())
|
}
|
||||||
.map(|s| s.to_string())
|
},
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
(
|
|
||||||
file_name,
|
|
||||||
i64::from(token.get_src_line()),
|
|
||||||
i64::from(token.get_src_col()),
|
|
||||||
source_line,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
let source_line = source_line
|
};
|
||||||
.or_else(|| getter.get_source_line(&file_name, line_number as usize));
|
(file_name, line_number + 1, column_number + 1)
|
||||||
(file_name, line_number + 1, column_number + 1, source_line)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_mappings<'a, G: SourceMapGetter + ?Sized>(
|
const MAX_SOURCE_LINE_LENGTH: usize = 150;
|
||||||
file_name: &str,
|
|
||||||
mappings_map: &'a mut CachedMaps,
|
|
||||||
getter: &G,
|
|
||||||
) -> &'a Option<SourceMap> {
|
|
||||||
mappings_map
|
|
||||||
.entry(file_name.to_string())
|
|
||||||
.or_insert_with(|| parse_map_string(file_name, getter))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(kitsonk) parsed source maps should probably be cached in state in
|
pub fn get_source_line<G: SourceMapGetter + ?Sized>(
|
||||||
// the module meta data.
|
|
||||||
fn parse_map_string<G: SourceMapGetter + ?Sized>(
|
|
||||||
file_name: &str,
|
file_name: &str,
|
||||||
|
line_number: i64,
|
||||||
|
cache: &mut SourceMapCache,
|
||||||
getter: &G,
|
getter: &G,
|
||||||
) -> Option<SourceMap> {
|
) -> Option<String> {
|
||||||
getter
|
cache
|
||||||
.get_source_map(file_name)
|
.source_lines
|
||||||
.and_then(|raw_source_map| SourceMap::from_slice(&raw_source_map).ok())
|
.entry((file_name.to_string(), line_number))
|
||||||
|
.or_insert_with(|| {
|
||||||
|
// Source lookup expects a 0-based line number, ours are 1-based.
|
||||||
|
let s = getter.get_source_line(file_name, (line_number - 1) as usize);
|
||||||
|
s.filter(|s| s.len() <= MAX_SOURCE_LINE_LENGTH)
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue