mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 01:42:14 +00:00
feat: show performance statistics in summary page (#743)
This commit is contained in:
parent
15785f1deb
commit
c798d3037e
13 changed files with 505 additions and 86 deletions
|
@ -23,8 +23,9 @@ use std::{
|
|||
};
|
||||
|
||||
use dashmap::{DashMap, SharedValue};
|
||||
use ecow::EcoString;
|
||||
use ecow::{EcoString, EcoVec};
|
||||
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||
use parking_lot::Mutex;
|
||||
use rustc_hash::FxHasher;
|
||||
use triomphe::Arc;
|
||||
use typst::{foundations::Str, syntax::ast::Ident};
|
||||
|
@ -55,12 +56,15 @@ impl<T: Internable> Interned<T> {
|
|||
RawEntryMut::Occupied(occ) => Self {
|
||||
arc: occ.key().clone(),
|
||||
},
|
||||
RawEntryMut::Vacant(vac) => Self {
|
||||
RawEntryMut::Vacant(vac) => {
|
||||
T::storage().alloc().increment();
|
||||
Self {
|
||||
arc: vac
|
||||
.insert_hashed_nocheck(hash, Arc::new(obj), SharedValue::new(()))
|
||||
.0
|
||||
.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,12 +89,16 @@ impl Interned<str> {
|
|||
RawEntryMut::Occupied(occ) => Self {
|
||||
arc: occ.key().clone(),
|
||||
},
|
||||
RawEntryMut::Vacant(vac) => Self {
|
||||
RawEntryMut::Vacant(vac) => {
|
||||
str::storage().alloc().increment();
|
||||
|
||||
Self {
|
||||
arc: vac
|
||||
.insert_hashed_nocheck(hash, Arc::from(s), SharedValue::new(()))
|
||||
.0
|
||||
.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -201,6 +209,8 @@ impl<T: Internable + ?Sized> Interned<T> {
|
|||
RawEntryMut::Vacant(_) => unreachable!(),
|
||||
};
|
||||
|
||||
T::storage().alloc().decrement();
|
||||
|
||||
// Shrink the backing storage if the shard is less than 50% occupied.
|
||||
if shard.len() * 2 < shard.capacity() {
|
||||
shard.shrink_to_fit();
|
||||
|
@ -331,26 +341,53 @@ impl<T: Display + Internable + ?Sized> Display for Interned<T> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) static MAPS: Mutex<EcoVec<(&'static str, usize, Arc<AllocStats>)>> =
|
||||
Mutex::new(EcoVec::new());
|
||||
|
||||
pub struct InternStorage<T: ?Sized> {
|
||||
alloc: OnceLock<Arc<AllocStats>>,
|
||||
map: OnceLock<InternMap<T>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::new_without_default)] // this a const fn, so it can't be default
|
||||
impl<T: ?Sized> InternStorage<T> {
|
||||
impl<T: InternSize + ?Sized> InternStorage<T> {
|
||||
const SIZE: usize = T::INTERN_SIZE;
|
||||
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
alloc: OnceLock::new(),
|
||||
map: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Internable + ?Sized> InternStorage<T> {
|
||||
fn alloc(&self) -> &Arc<AllocStats> {
|
||||
self.alloc.get_or_init(Arc::default)
|
||||
}
|
||||
|
||||
fn get(&self) -> &InternMap<T> {
|
||||
self.map.get_or_init(DashMap::default)
|
||||
self.map.get_or_init(|| {
|
||||
MAPS.lock()
|
||||
.push((std::any::type_name::<T>(), Self::SIZE, self.alloc().clone()));
|
||||
DashMap::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Internable: Hash + Eq + 'static {
|
||||
pub trait InternSize {
|
||||
const INTERN_SIZE: usize;
|
||||
}
|
||||
|
||||
impl<T: Sized> InternSize for T {
|
||||
const INTERN_SIZE: usize = std::mem::size_of::<T>();
|
||||
}
|
||||
|
||||
impl InternSize for str {
|
||||
const INTERN_SIZE: usize = std::mem::size_of::<usize>() * 2;
|
||||
}
|
||||
|
||||
pub trait Internable: InternSize + Hash + Eq + 'static {
|
||||
fn storage() -> &'static InternStorage<Self>;
|
||||
}
|
||||
|
||||
|
@ -370,5 +407,6 @@ macro_rules! _impl_internable {
|
|||
}
|
||||
|
||||
pub use crate::_impl_internable as impl_internable;
|
||||
use crate::analysis::AllocStats;
|
||||
|
||||
impl_internable!(str,);
|
||||
|
|
|
@ -8,6 +8,8 @@ pub mod color_exprs;
|
|||
pub use color_exprs::*;
|
||||
pub mod link_exprs;
|
||||
pub use link_exprs::*;
|
||||
pub mod stats;
|
||||
pub use stats::*;
|
||||
pub mod definition;
|
||||
pub use definition::*;
|
||||
pub mod signature;
|
||||
|
|
|
@ -29,8 +29,7 @@ use crate::analysis::{
|
|||
use crate::docs::{SignatureDocs, VarDocs};
|
||||
use crate::syntax::{
|
||||
construct_module_dependencies, find_expr_in_import, get_deref_target, resolve_id_by_path,
|
||||
scan_workspace_files, DerefTarget, ExprInfo, LexicalHierarchy, LexicalScope, ModuleDependency,
|
||||
Processing,
|
||||
scan_workspace_files, DerefTarget, ExprInfo, LexicalScope, ModuleDependency, Processing,
|
||||
};
|
||||
use crate::upstream::{tooltip_, Tooltip};
|
||||
use crate::{
|
||||
|
@ -38,7 +37,7 @@ use crate::{
|
|||
SemanticTokenContext, TypstRange, VersionedDocument,
|
||||
};
|
||||
|
||||
use super::{analyze_expr_, definition, Definition};
|
||||
use super::{analyze_expr_, definition, AllocStats, AnalysisStats, Definition, QueryStatGuard};
|
||||
|
||||
/// The analysis data holds globally.
|
||||
#[derive(Default, Clone)]
|
||||
|
@ -55,20 +54,11 @@ pub struct Analysis {
|
|||
pub cache_grid: Arc<Mutex<AnalysisGlobalCacheGrid>>,
|
||||
/// The semantic token context.
|
||||
pub tokens_ctx: Arc<SemanticTokenContext>,
|
||||
/// The statistics about the analyzers.
|
||||
pub analysis_stats: Arc<AnalysisStats>,
|
||||
}
|
||||
|
||||
impl Analysis {
|
||||
/// Get estimated memory usage of the analysis data.
|
||||
pub fn estimated_memory(&self) -> usize {
|
||||
let _ = LexicalHierarchy::estimated_memory;
|
||||
// todo: implement
|
||||
// self.caches.modules.capacity() * 32
|
||||
// + self .caches .modules .values() .map(|v| { v.def_use_lexical_hierarchy
|
||||
// .output .read() .as_ref() .map_or(0, |e| e.iter().map(|e|
|
||||
// e.estimated_memory()).sum()) }) .sum::<usize>()
|
||||
0
|
||||
}
|
||||
|
||||
/// Get a snapshot of the analysis data.
|
||||
pub fn snapshot<'a>(
|
||||
&self,
|
||||
|
@ -97,6 +87,16 @@ impl Analysis {
|
|||
revision,
|
||||
}
|
||||
}
|
||||
|
||||
/// Report the statistics of the analysis.
|
||||
pub fn report_query_stats(&self) -> String {
|
||||
self.analysis_stats.report()
|
||||
}
|
||||
|
||||
/// Report the statistics of the allocation.
|
||||
pub fn report_alloc_stats(&self) -> String {
|
||||
AllocStats::report(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The resources for analysis.
|
||||
|
@ -279,19 +279,19 @@ impl<'w> AnalysisContext<'w> {
|
|||
self.analysis
|
||||
.caches
|
||||
.def_signatures
|
||||
.retain(|_, (l, _, _)| lifetime - *l < 60);
|
||||
.retain(|(l, _)| lifetime - *l < 60);
|
||||
self.analysis
|
||||
.caches
|
||||
.static_signatures
|
||||
.retain(|_, (l, _, _, _)| lifetime - *l < 60);
|
||||
.retain(|(l, _)| lifetime - *l < 60);
|
||||
self.analysis
|
||||
.caches
|
||||
.terms
|
||||
.retain(|_, (l, _, _)| lifetime - *l < 60);
|
||||
.retain(|(l, _)| lifetime - *l < 60);
|
||||
self.analysis
|
||||
.caches
|
||||
.signatures
|
||||
.retain(|_, (l, _, _)| lifetime - *l < 60);
|
||||
.retain(|(l, _)| lifetime - *l < 60);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -409,6 +409,16 @@ impl SharedContext {
|
|||
self.slot.revision
|
||||
}
|
||||
|
||||
fn query_stat(&self, id: TypstFileId, query: &'static str) -> QueryStatGuard {
|
||||
let stats = &self.analysis.analysis_stats.query_stats;
|
||||
let entry = stats.entry(id).or_default();
|
||||
let entry = entry.entry(query).or_default();
|
||||
QueryStatGuard {
|
||||
bucket: entry.clone(),
|
||||
since: std::time::SystemTime::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the position encoding during session.
|
||||
pub(crate) fn position_encoding(&self) -> PositionEncoding {
|
||||
self.analysis.position_encoding
|
||||
|
@ -565,8 +575,9 @@ impl SharedContext {
|
|||
.analysis
|
||||
.caches
|
||||
.terms
|
||||
.m
|
||||
.get(&hash128(&cache_key))
|
||||
.and_then(|slot| (cache_key == &slot.1).then_some(slot.2.clone()));
|
||||
.and_then(|slot| (cache_key == &slot.1 .0).then_some(slot.1 .1.clone()));
|
||||
if let Some(cached) = cached {
|
||||
return cached;
|
||||
}
|
||||
|
@ -576,8 +587,9 @@ impl SharedContext {
|
|||
self.analysis
|
||||
.caches
|
||||
.terms
|
||||
.m
|
||||
.entry(hash128(&cache_key))
|
||||
.or_insert_with(|| (self.lifetime, cache_key.clone(), res.clone()));
|
||||
.or_insert_with(|| (self.lifetime, (cache_key.clone(), res.clone())));
|
||||
|
||||
res
|
||||
}
|
||||
|
@ -595,8 +607,9 @@ impl SharedContext {
|
|||
route: &mut Processing<Option<Arc<LazyHash<LexicalScope>>>>,
|
||||
) -> Arc<ExprInfo> {
|
||||
use crate::syntax::expr_of;
|
||||
let guard = self.query_stat(source.id(), "expr_stage");
|
||||
self.slot.expr_stage.compute(hash128(&source), |prev| {
|
||||
expr_of(self.clone(), source.clone(), route, prev)
|
||||
expr_of(self.clone(), source.clone(), route, guard, prev)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -627,6 +640,7 @@ impl SharedContext {
|
|||
use crate::analysis::type_check;
|
||||
|
||||
let ei = self.expr_stage(source);
|
||||
let guard = self.query_stat(source.id(), "type_check");
|
||||
self.slot.type_check.compute(hash128(&ei), |prev| {
|
||||
let cache_hit = prev.and_then(|prev| {
|
||||
// todo: recursively check changed scheme type
|
||||
|
@ -641,6 +655,7 @@ impl SharedContext {
|
|||
return prev.clone();
|
||||
}
|
||||
|
||||
guard.miss();
|
||||
type_check(self.clone(), ei, route)
|
||||
})
|
||||
}
|
||||
|
@ -785,46 +800,31 @@ impl SharedContext {
|
|||
.analysis
|
||||
.caches
|
||||
.def_signatures
|
||||
.entry(hash128(&(src, d.clone())))
|
||||
.or_insert_with(|| (self.lifetime, d, Arc::default()))
|
||||
.2
|
||||
.clone(),
|
||||
.entry(hash128(&(src, d.clone())), self.lifetime),
|
||||
SignatureTarget::SyntaxFast(source, span) => {
|
||||
let cache_key = (source, span, true);
|
||||
self.analysis
|
||||
.caches
|
||||
.static_signatures
|
||||
.entry(hash128(&cache_key))
|
||||
.or_insert_with(|| (self.lifetime, cache_key.0, cache_key.1, Arc::default()))
|
||||
.3
|
||||
.clone()
|
||||
.entry(hash128(&cache_key), self.lifetime)
|
||||
}
|
||||
SignatureTarget::Syntax(source, span) => {
|
||||
let cache_key = (source, span);
|
||||
self.analysis
|
||||
.caches
|
||||
.static_signatures
|
||||
.entry(hash128(&cache_key))
|
||||
.or_insert_with(|| (self.lifetime, cache_key.0, cache_key.1, Arc::default()))
|
||||
.3
|
||||
.clone()
|
||||
.entry(hash128(&cache_key), self.lifetime)
|
||||
}
|
||||
SignatureTarget::Convert(rt) => self
|
||||
.analysis
|
||||
.caches
|
||||
.signatures
|
||||
.entry(hash128(&(&rt, true)))
|
||||
.or_insert_with(|| (self.lifetime, rt, Arc::default()))
|
||||
.2
|
||||
.clone(),
|
||||
.entry(hash128(&(&rt, true)), self.lifetime),
|
||||
SignatureTarget::Runtime(rt) => self
|
||||
.analysis
|
||||
.caches
|
||||
.signatures
|
||||
.entry(hash128(&rt))
|
||||
.or_insert_with(|| (self.lifetime, rt, Arc::default()))
|
||||
.2
|
||||
.clone(),
|
||||
.entry(hash128(&rt), self.lifetime),
|
||||
};
|
||||
res.get_or_init(|| compute(self)).clone()
|
||||
}
|
||||
|
@ -943,7 +943,39 @@ impl<K, V> IncrCacheMap<K, V> {
|
|||
}
|
||||
}
|
||||
|
||||
type CacheMap<T> = Arc<FxDashMap<u128, T>>;
|
||||
#[derive(Clone)]
|
||||
struct CacheMap<T> {
|
||||
m: Arc<FxDashMap<u128, (u64, T)>>,
|
||||
// pub alloc: AllocStats,
|
||||
}
|
||||
|
||||
impl<T> Default for CacheMap<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
m: Default::default(),
|
||||
// alloc: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CacheMap<T> {
|
||||
fn clear(&self) {
|
||||
self.m.clear();
|
||||
}
|
||||
|
||||
fn retain(&self, mut f: impl FnMut(&mut (u64, T)) -> bool) {
|
||||
self.m.retain(|_k, v| f(v));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Default + Clone> CacheMap<T> {
|
||||
fn entry(&self, k: u128, lifetime: u64) -> T {
|
||||
let entry = self.m.entry(k);
|
||||
let entry = entry.or_insert_with(|| (lifetime, T::default()));
|
||||
entry.1.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// Needed by recursive computation
|
||||
type DeferredCompute<T> = Arc<OnceCell<T>>;
|
||||
|
||||
|
@ -953,10 +985,10 @@ type DeferredCompute<T> = Arc<OnceCell<T>>;
|
|||
pub struct AnalysisGlobalCaches {
|
||||
lifetime: Arc<AtomicU64>,
|
||||
clear_lifetime: Arc<AtomicU64>,
|
||||
def_signatures: CacheMap<(u64, Definition, DeferredCompute<Option<Signature>>)>,
|
||||
static_signatures: CacheMap<(u64, Source, Span, DeferredCompute<Option<Signature>>)>,
|
||||
signatures: CacheMap<(u64, Func, DeferredCompute<Option<Signature>>)>,
|
||||
terms: CacheMap<(u64, Value, Ty)>,
|
||||
def_signatures: CacheMap<DeferredCompute<Option<Signature>>>,
|
||||
static_signatures: CacheMap<DeferredCompute<Option<Signature>>>,
|
||||
signatures: CacheMap<DeferredCompute<Option<Signature>>>,
|
||||
terms: CacheMap<(Value, Ty)>,
|
||||
}
|
||||
|
||||
/// A cache for all level of analysis results of a module.
|
||||
|
|
200
crates/tinymist-query/src/analysis/stats.rs
Normal file
200
crates/tinymist-query/src/analysis/stats.rs
Normal file
|
@ -0,0 +1,200 @@
|
|||
//! Statistics about the analyzers
|
||||
|
||||
use std::{
|
||||
sync::{atomic::AtomicUsize, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use reflexo::hash::FxDashMap;
|
||||
use reflexo_typst::TypstFileId;
|
||||
|
||||
use super::Analysis;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct QueryStatBucketData {
|
||||
pub query: u64,
|
||||
pub missing: u64,
|
||||
pub total: Duration,
|
||||
pub min: Duration,
|
||||
pub max: Duration,
|
||||
}
|
||||
|
||||
impl Default for QueryStatBucketData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
query: 0,
|
||||
missing: 0,
|
||||
total: Duration::from_secs(0),
|
||||
min: Duration::from_secs(u64::MAX),
|
||||
max: Duration::from_secs(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about some query
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct QueryStatBucket {
|
||||
pub data: Arc<Mutex<QueryStatBucketData>>,
|
||||
}
|
||||
|
||||
pub(crate) struct QueryStatGuard {
|
||||
pub bucket: QueryStatBucket,
|
||||
pub since: std::time::SystemTime,
|
||||
}
|
||||
|
||||
impl Drop for QueryStatGuard {
|
||||
fn drop(&mut self) {
|
||||
let elapsed = self.since.elapsed().unwrap_or_default();
|
||||
let mut data = self.bucket.data.lock();
|
||||
data.query += 1;
|
||||
data.total += elapsed;
|
||||
data.min = data.min.min(elapsed);
|
||||
data.max = data.max.max(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryStatGuard {
|
||||
pub(crate) fn miss(&self) {
|
||||
let mut data = self.bucket.data.lock();
|
||||
data.missing += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about the analyzers
|
||||
#[derive(Default)]
|
||||
pub struct AnalysisStats {
|
||||
pub(crate) query_stats: FxDashMap<TypstFileId, FxDashMap<&'static str, QueryStatBucket>>,
|
||||
}
|
||||
|
||||
impl AnalysisStats {
|
||||
/// Report the statistics of the analysis.
|
||||
pub fn report(&self) -> String {
|
||||
let stats = &self.query_stats;
|
||||
let mut data = Vec::new();
|
||||
for refs in stats.iter() {
|
||||
let id = refs.key();
|
||||
let queries = refs.value();
|
||||
for refs2 in queries.iter() {
|
||||
let query = refs2.key();
|
||||
let bucket = refs2.value().data.lock().clone();
|
||||
let name = format!("{id:?}:{query}").replace('\\', "/");
|
||||
data.push((name, bucket));
|
||||
}
|
||||
}
|
||||
|
||||
// sort by query duration
|
||||
data.sort_by(|x, y| y.1.max.cmp(&x.1.max));
|
||||
|
||||
// format to html
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str(r#"<div>
|
||||
<style>
|
||||
table.analysis-stats { width: 100%; border-collapse: collapse; }
|
||||
table.analysis-stats th, table.analysis-stats td { border: 1px solid black; padding: 8px; text-align: center; }
|
||||
table.analysis-stats th.name-column, table.analysis-stats td.name-column { text-align: left; }
|
||||
table.analysis-stats tr:nth-child(odd) { background-color: rgba(242, 242, 242, 0.8); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
table.analysis-stats tr:nth-child(odd) { background-color: rgba(50, 50, 50, 0.8); }
|
||||
}
|
||||
</style>
|
||||
<table class="analysis-stats"><tr><th class="query-column">Query</th><th>Count</th><th>Missing</th><th>Total</th><th>Min</th><th>Max</th></tr>"#);
|
||||
|
||||
for (name, bucket) in data {
|
||||
html.push_str("<tr>");
|
||||
html.push_str(&format!(r#"<td class="query-column">{name}</td>"#));
|
||||
html.push_str(&format!("<td>{}</td>", bucket.query));
|
||||
html.push_str(&format!("<td>{}</td>", bucket.missing));
|
||||
html.push_str(&format!("<td>{:?}</td>", bucket.total));
|
||||
html.push_str(&format!("<td>{:?}</td>", bucket.min));
|
||||
html.push_str(&format!("<td>{:?}</td>", bucket.max));
|
||||
html.push_str("</tr>");
|
||||
}
|
||||
html.push_str("</table>");
|
||||
html.push_str("</div>");
|
||||
|
||||
html
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about the allocation
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AllocStats {
|
||||
/// The number of allocated objects.
|
||||
pub allocated: AtomicUsize,
|
||||
/// The number of dropped objects.
|
||||
pub dropped: AtomicUsize,
|
||||
}
|
||||
|
||||
impl AllocStats {
|
||||
/// increment the statistics.
|
||||
pub fn increment(&self) {
|
||||
self.allocated
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// decrement the statistics.
|
||||
pub fn decrement(&self) {
|
||||
self.dropped
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
impl AllocStats {
|
||||
/// Report the statistics of the allocation.
|
||||
pub fn report(_a: &Analysis) -> String {
|
||||
let maps = crate::adt::interner::MAPS.lock().clone();
|
||||
let mut data = Vec::new();
|
||||
for (name, sz, map) in maps {
|
||||
let allocated = map.allocated.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let dropped = map.dropped.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let alive = allocated.saturating_sub(dropped);
|
||||
data.push((name, sz * alive, allocated, dropped, alive));
|
||||
}
|
||||
|
||||
// sort by total
|
||||
data.sort_by(|x, y| y.4.cmp(&x.4));
|
||||
|
||||
// format to html
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str(r#"<div>
|
||||
<style>
|
||||
table.alloc-stats { width: 100%; border-collapse: collapse; }
|
||||
table.alloc-stats th, table.alloc-stats td { border: 1px solid black; padding: 8px; text-align: center; }
|
||||
table.alloc-stats th.name-column, table.alloc-stats td.name-column { text-align: left; }
|
||||
table.alloc-stats tr:nth-child(odd) { background-color: rgba(242, 242, 242, 0.8); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
table.alloc-stats tr:nth-child(odd) { background-color: rgba(50, 50, 50, 0.8); }
|
||||
}
|
||||
</style>
|
||||
<table class="alloc-stats"><tr><th class="name-column">Name</th><th>Alive</th><th>Allocated</th><th>Dropped</th><th>Size</th></tr>"#);
|
||||
|
||||
for (name, sz, allocated, dropped, alive) in data {
|
||||
html.push_str("<tr>");
|
||||
html.push_str(&format!(r#"<td class="name-column">{name}</td>"#));
|
||||
html.push_str(&format!("<td>{alive}</td>"));
|
||||
html.push_str(&format!("<td>{allocated}</td>"));
|
||||
html.push_str(&format!("<td>{dropped}</td>"));
|
||||
html.push_str(&format!("<td>{}</td>", human_size(sz)));
|
||||
html.push_str("</tr>");
|
||||
}
|
||||
html.push_str("</table>");
|
||||
html.push_str("</div>");
|
||||
|
||||
html
|
||||
}
|
||||
}
|
||||
|
||||
fn human_size(size: usize) -> String {
|
||||
let units = ["B", "KB", "MB", "GB", "TB"];
|
||||
let mut unit = 0;
|
||||
let mut size = size as f64;
|
||||
while size >= 768.0 && unit < units.len() {
|
||||
size /= 1024.0;
|
||||
unit += 1;
|
||||
}
|
||||
format!("{:.2} {}", size, units[unit])
|
||||
}
|
|
@ -229,7 +229,7 @@ mod polymorphic {
|
|||
pub root: Option<PathBuf>,
|
||||
pub font_paths: Vec<PathBuf>,
|
||||
pub inputs: Dict,
|
||||
pub estimated_memory_usage: HashMap<String, usize>,
|
||||
pub stats: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
|
@ -240,7 +240,7 @@ mod polymorphic {
|
|||
ContextFreeUnique,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, strum::IntoStaticStr)]
|
||||
pub enum CompilerQueryRequest {
|
||||
OnExport(OnExportRequest),
|
||||
Hover(HoverRequest),
|
||||
|
|
|
@ -14,7 +14,7 @@ use typst::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
analysis::SharedContext,
|
||||
analysis::{QueryStatGuard, SharedContext},
|
||||
docs::DocStringKind,
|
||||
prelude::*,
|
||||
syntax::find_module_level_docs,
|
||||
|
@ -29,6 +29,7 @@ pub(crate) fn expr_of(
|
|||
ctx: Arc<SharedContext>,
|
||||
source: Source,
|
||||
route: &mut Processing<Option<Arc<LazyHash<LexicalScope>>>>,
|
||||
guard: QueryStatGuard,
|
||||
prev: Option<Arc<ExprInfo>>,
|
||||
) -> Arc<ExprInfo> {
|
||||
log::debug!("expr_of: {:?}", source.id());
|
||||
|
@ -62,6 +63,7 @@ pub(crate) fn expr_of(
|
|||
route.remove(&source.id());
|
||||
return prev;
|
||||
}
|
||||
guard.miss();
|
||||
|
||||
let revision = ctx.revision();
|
||||
|
||||
|
|
|
@ -146,18 +146,6 @@ pub(crate) struct LexicalHierarchy {
|
|||
pub children: Option<LazyHash<EcoVec<LexicalHierarchy>>>,
|
||||
}
|
||||
|
||||
impl LexicalHierarchy {
|
||||
pub fn estimated_memory(&self) -> usize {
|
||||
std::mem::size_of::<Self>()
|
||||
+ std::mem::size_of::<LexicalInfo>()
|
||||
+ self.info.name.len()
|
||||
+ self
|
||||
.children
|
||||
.as_ref()
|
||||
.map_or(0, |c| c.iter().map(|e| e.estimated_memory()).sum())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for LexicalHierarchy {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
use serde::ser::SerializeStruct;
|
||||
|
|
|
@ -105,6 +105,7 @@ impl LanguageState {
|
|||
intr_tx: intr_tx.clone(),
|
||||
export: export.clone(),
|
||||
editor_tx: self.editor_tx.clone(),
|
||||
stats: Default::default(),
|
||||
analysis: Arc::new(Analysis {
|
||||
position_encoding,
|
||||
enable_periscope,
|
||||
|
@ -116,6 +117,7 @@ impl LanguageState {
|
|||
const_config.tokens_overlapping_token_support,
|
||||
const_config.tokens_multiline_token_support,
|
||||
)),
|
||||
analysis_stats: Default::default(),
|
||||
}),
|
||||
periscope: PeriscopeRenderer::new(periscope_args.unwrap_or_default()),
|
||||
|
||||
|
|
|
@ -33,8 +33,8 @@ use reflexo_typst::{
|
|||
use sync_lsp::{just_future, QueryFuture};
|
||||
use tinymist_query::{
|
||||
analysis::{Analysis, AnalysisContext, AnalysisResources},
|
||||
CompilerQueryResponse, DiagnosticsMap, ExportKind, SemanticRequest, ServerInfoResponse,
|
||||
StatefulRequest, VersionedDocument,
|
||||
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, ExportKind, SemanticRequest,
|
||||
ServerInfoResponse, StatefulRequest, VersionedDocument,
|
||||
};
|
||||
use tinymist_render::PeriscopeRenderer;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
@ -47,6 +47,7 @@ use super::{
|
|||
},
|
||||
};
|
||||
use crate::{
|
||||
stats::{CompilerQueryStats, QueryStatGuard},
|
||||
task::{ExportTask, ExportUserConfig},
|
||||
world::{LspCompilerFeat, LspWorld},
|
||||
CompileConfig,
|
||||
|
@ -57,6 +58,7 @@ type EditorSender = mpsc::UnboundedSender<EditorRequest>;
|
|||
pub struct CompileHandler {
|
||||
pub(crate) diag_group: String,
|
||||
pub(crate) analysis: Arc<Analysis>,
|
||||
pub(crate) stats: CompilerQueryStats,
|
||||
pub(crate) periscope: PeriscopeRenderer,
|
||||
|
||||
#[cfg(feature = "preview")]
|
||||
|
@ -339,6 +341,15 @@ impl CompileClientActor {
|
|||
self.handle.clone().snapshot()
|
||||
}
|
||||
|
||||
/// Snapshot the compiler thread for tasks
|
||||
pub fn snapshot_with_stat(&self, q: &CompilerQueryRequest) -> ZResult<QuerySnapWithStat> {
|
||||
let name: &'static str = q.into();
|
||||
let path = q.associated_path();
|
||||
let stat = self.handle.stats.query_stat(path, name);
|
||||
let snap = self.handle.clone().snapshot()?;
|
||||
Ok(QuerySnapWithStat { snap, stat })
|
||||
}
|
||||
|
||||
pub fn add_memory_changes(&self, event: MemoryEvent) {
|
||||
self.handle.add_memory_changes(event);
|
||||
}
|
||||
|
@ -414,6 +425,9 @@ impl CompileClientActor {
|
|||
|
||||
pub fn collect_server_info(&self) -> QueryFuture {
|
||||
let dg = self.handle.diag_group.clone();
|
||||
let api_stats = self.handle.stats.report();
|
||||
let query_stats = self.handle.analysis.report_query_stats();
|
||||
let alloc_stats = self.handle.analysis.report_alloc_stats();
|
||||
|
||||
let snap = self.snapshot()?;
|
||||
just_future(async move {
|
||||
|
@ -424,11 +438,10 @@ impl CompileClientActor {
|
|||
root: w.entry_state().root().map(|e| e.as_ref().to_owned()),
|
||||
font_paths: w.font_resolver.font_paths().to_owned(),
|
||||
inputs: w.inputs().as_ref().deref().clone(),
|
||||
estimated_memory_usage: HashMap::from_iter([
|
||||
// todo: vfs memory usage
|
||||
// ("vfs".to_owned(), w.vfs.read().memory_usage()),
|
||||
// todo: analysis memory usage
|
||||
// ("analysis".to_owned(), cc.analysis.estimated_memory()),
|
||||
stats: HashMap::from_iter([
|
||||
("api".to_owned(), api_stats),
|
||||
("query".to_owned(), query_stats),
|
||||
("alloc".to_owned(), alloc_stats),
|
||||
]),
|
||||
};
|
||||
|
||||
|
@ -438,6 +451,11 @@ impl CompileClientActor {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct QuerySnapWithStat {
|
||||
pub snap: QuerySnap,
|
||||
pub(crate) stat: QueryStatGuard,
|
||||
}
|
||||
|
||||
pub struct QuerySnap {
|
||||
rx: oneshot::Receiver<CompileSnapshot<LspCompilerFeat>>,
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ mod cmd;
|
|||
mod init;
|
||||
mod resource;
|
||||
mod server;
|
||||
mod stats;
|
||||
mod task;
|
||||
pub use task::UserActionTask;
|
||||
pub mod tool;
|
||||
|
|
|
@ -1023,7 +1023,7 @@ impl LanguageState {
|
|||
type R = CompilerQueryResponse;
|
||||
assert!(query.fold_feature() != FoldRequestFeature::ContextFreeUnique);
|
||||
|
||||
let snap = client.snapshot()?;
|
||||
let snap_stat = client.snapshot_with_stat(&query)?;
|
||||
let handle = client.handle.clone();
|
||||
let entry = query
|
||||
.associated_path()
|
||||
|
@ -1036,7 +1036,7 @@ impl LanguageState {
|
|||
let rev_lock = handle.analysis.lock_revision();
|
||||
|
||||
just_future(async move {
|
||||
let mut snap = snap.receive().await?;
|
||||
let mut snap = snap_stat.snap.receive().await?;
|
||||
// todo: whether it is safe to inherit success_doc with changed entry
|
||||
if !is_pinning {
|
||||
snap = snap.task(TaskInputs {
|
||||
|
@ -1044,6 +1044,7 @@ impl LanguageState {
|
|||
..Default::default()
|
||||
});
|
||||
}
|
||||
snap_stat.stat.snap();
|
||||
|
||||
let resp = match query {
|
||||
SemanticTokensFull(req) => handle.run_semantic(snap, req, R::SemanticTokensFull),
|
||||
|
|
134
crates/tinymist/src/stats.rs
Normal file
134
crates/tinymist/src/stats.rs
Normal file
|
@ -0,0 +1,134 @@
|
|||
//! Statistics about the analyzers
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use reflexo::{hash::FxDashMap, path::unix_slash};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct QueryStatBucketData {
|
||||
pub query: u64,
|
||||
pub total: Duration,
|
||||
pub snap: Duration,
|
||||
pub min: Duration,
|
||||
pub max: Duration,
|
||||
}
|
||||
|
||||
impl Default for QueryStatBucketData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
query: 0,
|
||||
total: Duration::from_secs(0),
|
||||
snap: Duration::from_secs(0),
|
||||
min: Duration::from_secs(u64::MAX),
|
||||
max: Duration::from_secs(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about some query
|
||||
#[derive(Default, Clone)]
|
||||
pub(crate) struct QueryStatBucket {
|
||||
pub data: Arc<Mutex<QueryStatBucketData>>,
|
||||
}
|
||||
|
||||
pub(crate) struct QueryStatGuard {
|
||||
pub bucket: QueryStatBucket,
|
||||
pub since: std::time::SystemTime,
|
||||
pub snap_since: OnceLock<std::time::Duration>,
|
||||
}
|
||||
|
||||
impl Drop for QueryStatGuard {
|
||||
fn drop(&mut self) {
|
||||
let elapsed = self.since.elapsed().unwrap_or_default();
|
||||
let mut data = self.bucket.data.lock();
|
||||
data.query += 1;
|
||||
data.total += elapsed;
|
||||
data.snap += self.snap_since.get().cloned().unwrap_or_default();
|
||||
data.min = data.min.min(elapsed);
|
||||
data.max = data.max.max(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
impl QueryStatGuard {
|
||||
pub(crate) fn snap(&self) {
|
||||
self.snap_since
|
||||
.get_or_init(|| self.since.elapsed().unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics about the analyzers
|
||||
#[derive(Default)]
|
||||
pub struct CompilerQueryStats {
|
||||
pub(crate) query_stats: FxDashMap<PathBuf, FxDashMap<&'static str, QueryStatBucket>>,
|
||||
}
|
||||
|
||||
impl CompilerQueryStats {
|
||||
/// Record a query.
|
||||
pub fn query_stat(&self, path: Option<&Path>, name: &'static str) -> QueryStatGuard {
|
||||
let stats = &self.query_stats;
|
||||
// let refs = stats.entry(path.clone()).or_default();
|
||||
let refs = stats
|
||||
.entry(path.unwrap_or_else(|| Path::new("")).to_path_buf())
|
||||
.or_default();
|
||||
let refs2 = refs.entry(name).or_default();
|
||||
QueryStatGuard {
|
||||
bucket: refs2.clone(),
|
||||
since: std::time::SystemTime::now(),
|
||||
snap_since: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Report the statistics of the analysis.
|
||||
pub fn report(&self) -> String {
|
||||
let stats = &self.query_stats;
|
||||
let mut data = Vec::new();
|
||||
for refs in stats.iter() {
|
||||
let id = unix_slash(refs.key());
|
||||
let queries = refs.value();
|
||||
for refs2 in queries.iter() {
|
||||
let query = refs2.key();
|
||||
let bucket = refs2.value().data.lock().clone();
|
||||
let name = format!("{id:?}:{query}").replace('\\', "/");
|
||||
data.push((name, bucket));
|
||||
}
|
||||
}
|
||||
|
||||
// sort by query duration
|
||||
data.sort_by(|x, y| y.1.max.cmp(&x.1.max));
|
||||
|
||||
// format to html
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str(r#"<div>
|
||||
<style>
|
||||
table.query-stats { width: 100%; border-collapse: collapse; }
|
||||
table.query-stats th, table.query-stats td { border: 1px solid black; padding: 8px; text-align: center; }
|
||||
table.query-stats th.name-column, table.query-stats td.name-column { text-align: left; }
|
||||
table.query-stats tr:nth-child(odd) { background-color: rgba(242, 242, 242, 0.8); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
table.query-stats tr:nth-child(odd) { background-color: rgba(50, 50, 50, 0.8); }
|
||||
}
|
||||
</style>
|
||||
<table class="query-stats"><tr><th class="query-column">Query</th><th>Count</th><th>Total</th><th>Snap</th><th>Min</th><th>Max</th></tr>"#);
|
||||
|
||||
for (name, bucket) in data {
|
||||
html.push_str("<tr>");
|
||||
html.push_str(&format!(r#"<td class="query-column">{name}</td>"#));
|
||||
html.push_str(&format!("<td>{}</td>", bucket.query));
|
||||
html.push_str(&format!("<td>{:?}</td>", bucket.total));
|
||||
html.push_str(&format!("<td>{:?}</td>", bucket.snap));
|
||||
html.push_str(&format!("<td>{:?}</td>", bucket.min));
|
||||
html.push_str(&format!("<td>{:?}</td>", bucket.max));
|
||||
html.push_str("</tr>");
|
||||
}
|
||||
html.push_str("</table>");
|
||||
html.push_str("</div>");
|
||||
|
||||
html
|
||||
}
|
||||
}
|
|
@ -539,6 +539,7 @@ pub async fn preview_main(args: PreviewCliArgs) -> anyhow::Result<()> {
|
|||
export: Default::default(),
|
||||
editor_tx,
|
||||
analysis: Arc::default(),
|
||||
stats: Default::default(),
|
||||
periscope: tinymist_render::PeriscopeRenderer::default(),
|
||||
notified_revision: parking_lot::Mutex::new(0),
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue