1use crate::{
26 CachedEntry, DexBytes, FindOptions, LogicalDex, Matched, MemoryPolicy, RefBatch, RefResult, RefSite, Shape, SharedMemory, TextRows,
27 available_memory,
28};
29use asc_cache::{ApkKey, DiskCache, ProcessRegistry};
30use asc_dex::Dex;
31use asc_findrefs::{ClassRelation, Query, SearchState};
32use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
33use std::sync::{Arc, Mutex, OnceLock};
34use std::time::{Duration, Instant};
35
36pub(crate) const LOCATORS_KIND: &str = "locators";
38pub(crate) const L3_MAX_RESULT: usize = 4 << 20;
40const L3_MAX_TEXT: usize = 32 << 20;
44const RESULT_FORMAT: u32 = 6;
46const RESULT_END: u32 = u32::MAX;
49
50#[derive(Default)]
53pub(crate) struct Persisted {
54 tried: AtomicBool,
56 queued: AtomicU32,
58 cost_us: AtomicU64,
60}
61
62pub(crate) fn load_derived(disk: &DiskCache, key: &ApkKey, l: &LogicalDex, dex: &Dex<'_>) -> bool {
64 if l.persisted.tried.swap(true, Ordering::Relaxed) {
65 return false;
66 }
67 let Some(d) = disk.get_derived(key, &l.name, LOCATORS_KIND, asc_findrefs::PERSIST_FORMAT_VERSION) else { return false };
68 let cost = d.build_cost.as_micros() as u64;
69 let Some(state) = SearchState::from_persisted(dex, Arc::new(d)) else { return false };
70 l.persisted.queued.fetch_or(state.table_mask(), Ordering::Relaxed);
71 l.persisted.cost_us.fetch_add(cost, Ordering::Relaxed);
72 l.state.fill_from(state);
73 true
74}
75
76pub(crate) struct DerivedJob {
78 entry: String,
80 logical: usize,
81 name: String,
82 data: DexBytes,
83 state: Arc<SearchState>,
84 persisted: Arc<Persisted>,
85 claimed: u32,
87}
88
89impl DerivedJob {
90 pub(crate) fn held_bytes(&self) -> usize {
92 self.state.heap_bytes()
93 }
94
95 pub(crate) fn abandon(self) {
97 self.persisted.queued.fetch_and(!self.claimed, Ordering::Relaxed);
98 }
99}
100
101pub(crate) fn derived_job(disk: &DiskCache, entry: &CachedEntry, idx: usize, mask_before: u32, spent: Duration) -> Option<DerivedJob> {
105 let l = entry.logical().get(idx)?;
106 let mask = l.state.table_mask();
107 let p = &l.persisted;
108 if mask == mask_before && mask & !p.queued.load(Ordering::Relaxed) == 0 {
109 return None;
110 }
111 let cost = if mask != mask_before {
112 p.cost_us.fetch_add(spent.as_micros() as u64, Ordering::Relaxed) + spent.as_micros() as u64
113 } else {
114 p.cost_us.load(Ordering::Relaxed)
115 };
116 if !disk.admits_derived(Duration::from_micros(cost)) {
117 return None;
118 }
119 let claimed = mask & !p.queued.fetch_or(mask, Ordering::Relaxed);
120 if claimed == 0 {
121 return None;
122 }
123 Some(DerivedJob {
124 entry: entry.name.clone(),
125 logical: idx,
126 name: l.name.clone(),
127 data: Arc::clone(&l.data),
128 state: Arc::clone(&l.state),
129 persisted: Arc::clone(&l.persisted),
130 claimed,
131 })
132}
133
134pub(crate) fn write_derived(disk: &DiskCache, memory: Option<&SharedMemory>, key: &ApkKey, job: DerivedJob) {
137 let Ok(dex) = Dex::with_name(&job.data, &job.name) else { return };
138 let mask = job.state.table_mask();
139 let bytes = job.state.persist(&dex);
140 let cost = Duration::from_micros(job.persisted.cost_us.load(Ordering::Relaxed));
141 let version = asc_findrefs::PERSIST_FORMAT_VERSION;
142 if disk.put_derived(key, &job.name, LOCATORS_KIND, version, cost, &bytes).is_err() {
143 return;
144 }
145 drop(bytes);
146 let Some(memory) = memory else { return };
147 let Some(mapped) =
148 disk.get_derived(key, &job.name, LOCATORS_KIND, version).and_then(|d| SearchState::from_persisted(&dex, Arc::new(d)))
149 else {
150 return;
151 };
152 if mapped.table_mask() != mask || job.state.table_mask() != mask {
155 return;
156 }
157 let mut mem = memory.lock().unwrap();
158 let Some(current) = mem.peek(key, &job.entry) else { return };
159 let still_ours = current.logical.get().and_then(|l| l.get(job.logical)).is_some_and(|l| Arc::ptr_eq(&l.state, &job.state));
160 if still_ours {
161 let new = Arc::new(current.with_state(job.logical, Arc::new(mapped)));
162 let size = new.charged_bytes();
163 if mem.replace(key, &job.entry, ¤t, Arc::clone(&new)) {
164 mem.resize(key, &job.entry, &new, size);
165 }
166 }
167}
168
169fn tool_version() -> &'static str {
173 static VERSION: OnceLock<String> = OnceLock::new();
174 VERSION.get_or_init(|| {
175 let exe = std::env::current_exe().ok().and_then(|p| std::fs::metadata(p).ok());
176 let stamp = exe.map(|m| {
177 let mtime = m.modified().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map_or(0, |d| d.as_nanos());
178 format!("{}.{mtime}", m.len())
179 });
180 format!("{}+{}", env!("CARGO_PKG_VERSION"), stamp.unwrap_or_default())
181 })
182}
183
184pub(crate) fn findrefs_key(query: &Query, opts: FindOptions, shape: Shape) -> String {
186 let query = match shape {
187 Shape::Results => format!("{query:?}\0{opts:?}"),
188 Shape::Text => format!("{query:?}\0{opts:?}\0text"),
189 };
190 asc_cache::rendered_key("findrefs", tool_version(), &query)
191}
192
193pub(crate) fn result_limit(disk: &DiskCache, shape: Shape) -> usize {
195 match shape {
196 Shape::Results => L3_MAX_RESULT,
197 Shape::Text => usize::try_from(disk.capacity_for(0) / 8).unwrap_or(usize::MAX).min(L3_MAX_TEXT),
198 }
199}
200
201pub(crate) struct ResultRecorder {
206 out: Vec<u8>,
207 keep: bool,
208 limit: usize,
209}
210
211impl ResultRecorder {
212 pub(crate) fn new(limit: usize) -> ResultRecorder {
213 let mut out = Vec::new();
214 out.extend_from_slice(&RESULT_FORMAT.to_le_bytes());
215 ResultRecorder { out, keep: true, limit }
216 }
217
218 fn abandon(&mut self) {
220 self.keep = false;
221 self.out = Vec::new();
222 }
223
224 pub(crate) fn record(&mut self, b: &RefBatch) {
225 if !self.keep {
226 return;
227 }
228 let text_bytes = b.text.as_ref().map_or(0, |t| t.as_str().len() + 4 * t.len());
230 if b.error.is_some() || self.out.len().saturating_add(text_bytes) > self.limit {
231 self.abandon();
232 return;
233 }
234 put_str(&mut self.out, &b.entry_name);
235 put_u32(&mut self.out, b.results.len() as u32);
236 for r in &b.results {
237 put_str(&mut self.out, &r.dex_name);
238 put_u32(&mut self.out, r.method_idx);
239 put_str(&mut self.out, &r.method);
240 put_str(&mut self.out, &r.signature);
241 put_u32(&mut self.out, r.matched.len() as u32);
242 for m in &r.matched {
243 self.out.push(match m {
244 Matched::String(_) => 0,
245 Matched::Symbol(_) => 1,
246 Matched::Field { .. } => 2,
247 });
248 put_str(&mut self.out, m.as_str());
249 if let Matched::Field { access, .. } = m {
250 self.out.push(match access {
251 asc_findrefs::FieldAccess::Read => 1,
252 asc_findrefs::FieldAccess::Write => 2,
253 });
254 }
255 }
256 self.out.push(match r.relation {
257 None => 0,
258 Some(ClassRelation::Extends) => 1,
259 Some(ClassRelation::Implements) => 2,
260 });
261 put_u32(&mut self.out, r.sites.len() as u32);
262 for site in &r.sites {
263 put_u32(&mut self.out, site.offset);
264 put_u32(&mut self.out, site.matched as u32);
265 self.out.push(match site.field_access {
266 None => 0,
267 Some(asc_findrefs::FieldAccess::Read) => 1,
268 Some(asc_findrefs::FieldAccess::Write) => 2,
269 });
270 put_opt(&mut self.out, site.pc);
271 put_opt(&mut self.out, site.line);
272 }
273 match &r.context {
274 None => self.out.push(0),
275 Some(c) => {
276 self.out.push(1);
277 put_str(&mut self.out, c);
278 }
279 }
280 }
281 match &b.text {
282 None => self.out.push(0),
283 Some(t) => {
284 let Ok(ends) = t.ends().iter().map(|&e| u32::try_from(e)).collect::<Result<Vec<_>, _>>() else {
285 return self.abandon();
286 };
287 self.out.push(1);
288 put_str(&mut self.out, t.as_str());
289 put_u32(&mut self.out, ends.len() as u32);
290 for e in ends {
291 put_u32(&mut self.out, e);
292 }
293 }
294 }
295 if self.out.len() > self.limit {
296 self.abandon();
297 }
298 }
299
300 pub(crate) fn finish(mut self, notes: &[String]) -> Option<Vec<u8>> {
302 put_u32(&mut self.out, RESULT_END);
303 put_u32(&mut self.out, notes.len() as u32);
304 for n in notes {
305 put_str(&mut self.out, n);
306 }
307 self.keep.then_some(self.out)
308 }
309}
310
311fn put_u32(out: &mut Vec<u8>, v: u32) {
312 out.extend_from_slice(&v.to_le_bytes());
313}
314
315fn put_opt(out: &mut Vec<u8>, v: Option<u32>) {
316 match v {
317 None => out.push(0),
318 Some(v) => {
319 out.push(1);
320 put_u32(out, v);
321 }
322 }
323}
324
325fn put_str(out: &mut Vec<u8>, s: &str) {
326 put_u32(out, s.len() as u32);
327 out.extend_from_slice(s.as_bytes());
328}
329
330pub(crate) struct Recorded {
332 pub batches: Vec<RefBatch>,
333 pub notes: Vec<String>,
334}
335
336pub(crate) fn decode_batches(payload: impl AsRef<[u8]> + Send + Sync + 'static) -> Option<Recorded> {
339 let payload: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(payload);
340 let bytes = (*payload).as_ref();
341 let mut r = Cursor { bytes, at: 0 };
342 if r.u32()? != RESULT_FORMAT {
343 return None;
344 }
345 let mut out = Vec::new();
346 loop {
347 let len = r.u32()?;
348 if len == RESULT_END {
349 let notes = (0..r.u32()?).map(|_| r.str()).collect::<Option<Vec<_>>>()?;
350 return (r.at == bytes.len()).then_some(Recorded { batches: out, notes });
351 }
352 let entry_name = String::from_utf8(r.take(len as usize)?.to_vec()).ok()?;
353 let n = r.u32()? as usize;
354 let mut results = Vec::with_capacity(n.min(bytes.len()));
355 for _ in 0..n {
356 let dex_name = r.str()?;
357 let method_idx = r.u32()?;
358 let method = r.str()?;
359 let signature = r.str()?;
360 let m = r.u32()? as usize;
361 let matched = (0..m)
362 .map(|_| match r.u8()? {
363 0 => Some(Matched::String(r.str()?)),
364 1 => Some(Matched::Symbol(r.str()?)),
365 2 => {
366 let symbol = r.str()?;
367 let access = match r.u8()? {
368 1 => asc_findrefs::FieldAccess::Read,
369 2 => asc_findrefs::FieldAccess::Write,
370 _ => return None,
371 };
372 Some(Matched::Field { symbol, access })
373 }
374 _ => None,
375 })
376 .collect::<Option<Vec<_>>>()?;
377 let relation = match r.u8()? {
378 0 => None,
379 1 => Some(ClassRelation::Extends),
380 2 => Some(ClassRelation::Implements),
381 _ => return None,
382 };
383 let sites = (0..r.u32()?)
384 .map(|_| {
385 let offset = r.u32()?;
386 let matched = r.u32()? as usize;
387 let field_access = match r.u8()? {
388 0 => None,
389 1 => Some(asc_findrefs::FieldAccess::Read),
390 2 => Some(asc_findrefs::FieldAccess::Write),
391 _ => return None,
392 };
393 Some(RefSite { offset, matched, field_access, pc: r.opt()?, line: r.opt()? })
394 })
395 .collect::<Option<Vec<_>>>()?;
396 let context = match r.u8()? {
397 0 => None,
398 1 => Some(r.str()?),
399 _ => return None,
400 };
401 results.push(RefResult { dex_name, method_idx, method, signature, matched, relation, sites, context });
402 }
403 let text = match r.u8()? {
404 0 => None,
405 1 => {
406 let len = r.u32()? as usize;
407 let start = r.at;
408 r.take(len)?;
409 let ends = (0..r.u32()?).map(|_| r.u32().map(|e| e as usize)).collect::<Option<Vec<_>>>()?;
410 Some(TextRows::shared(Arc::clone(&payload), start..start + len, ends)?)
411 }
412 _ => return None,
413 };
414 out.push(RefBatch { entry_name, results, text, inflate_us: 0, search_us: 0, source: "result", error: None });
415 }
416}
417
418struct Cursor<'b> {
419 bytes: &'b [u8],
420 at: usize,
421}
422
423impl Cursor<'_> {
424 fn take(&mut self, n: usize) -> Option<&[u8]> {
425 let s = self.bytes.get(self.at..self.at.checked_add(n)?)?;
426 self.at += n;
427 Some(s)
428 }
429
430 fn u8(&mut self) -> Option<u8> {
431 Some(self.take(1)?[0])
432 }
433
434 fn u32(&mut self) -> Option<u32> {
435 Some(u32::from_le_bytes(self.take(4)?.try_into().ok()?))
436 }
437
438 fn opt(&mut self) -> Option<Option<u32>> {
440 match self.u8()? {
441 0 => Some(None),
442 1 => Some(Some(self.u32()?)),
443 _ => None,
444 }
445 }
446
447 fn str(&mut self) -> Option<String> {
448 let n = self.u32()? as usize;
449 String::from_utf8(self.take(n)?.to_vec()).ok()
450 }
451}
452
453const REBALANCE_EVERY: Duration = Duration::from_secs(1);
455
456struct Registration {
458 caches: usize,
459 registry: Option<ProcessRegistry>,
460 held: u64,
462 requests: usize,
464}
465
466impl Registration {
467 fn publish(&self) {
468 if let Some(registry) = &self.registry {
469 let _ = registry.update(self.held, self.requests > 0);
470 }
471 }
472}
473
474static REGISTRATION: Mutex<Registration> = Mutex::new(Registration { caches: 0, registry: None, held: 0, requests: 0 });
475
476#[must_use = "the process is busy only while the guard lives"]
481pub struct Busy(());
482
483impl Busy {
484 pub fn enter() -> Busy {
485 let mut reg = REGISTRATION.lock().unwrap();
486 reg.requests += 1;
487 if reg.requests == 1 {
488 reg.publish();
489 }
490 Busy(())
491 }
492}
493
494impl Drop for Busy {
495 fn drop(&mut self) {
496 let mut reg = REGISTRATION.lock().unwrap();
497 reg.requests -= 1;
498 if reg.requests == 0 {
499 reg.publish();
500 }
501 }
502}
503
504pub(crate) struct MemoryBudget {
508 auto: Option<MemoryPolicy>,
509 last: Mutex<Option<Instant>>,
510 published: AtomicU64,
511}
512
513impl MemoryBudget {
514 pub(crate) fn new(auto: Option<MemoryPolicy>) -> MemoryBudget {
515 let mut reg = REGISTRATION.lock().unwrap();
516 reg.caches += 1;
517 if reg.registry.is_none() {
518 reg.registry = ProcessRegistry::default_dir().and_then(ProcessRegistry::open);
519 }
520 MemoryBudget { auto, last: Mutex::new(None), published: AtomicU64::new(0) }
521 }
522
523 pub(crate) fn maybe_rebalance(&self, memory: &SharedMemory) {
526 {
527 let mut last = self.last.lock().unwrap();
528 if last.is_some_and(|t| t.elapsed() < REBALANCE_EVERY) {
529 return;
530 }
531 *last = Some(Instant::now());
532 }
533 let used = {
534 let m = memory.lock().unwrap();
535 (m.used_bytes() + m.reserved_bytes()) as u64
536 };
537 let previous = self.published.swap(used, Ordering::Relaxed);
538 let (held, others) = {
539 let mut reg = REGISTRATION.lock().unwrap();
540 reg.held = (reg.held + used).saturating_sub(previous);
541 reg.publish();
542 let Some(registry) = ®.registry else { return };
543 (reg.held, registry.others())
544 };
545 if let Some(policy) = self.auto {
546 let cap = policy.share(available_memory(), held, &others);
547 memory.lock().unwrap().set_capacity(cap);
548 }
549 }
550}
551
552impl Drop for MemoryBudget {
553 fn drop(&mut self) {
554 let mut reg = REGISTRATION.lock().unwrap();
555 reg.held = reg.held.saturating_sub(self.published.load(Ordering::Relaxed));
556 reg.caches -= 1;
557 if reg.caches == 0 {
558 reg.registry = None;
560 } else {
561 reg.publish();
562 }
563 }
564}
565
566const BUSY_WINDOW: Duration = Duration::from_secs(5);
571
572const THREADS_EVERY: Duration = Duration::from_millis(100);
578
579pub fn auto_threads() -> usize {
585 static LAST: Mutex<Option<(Instant, usize)>> = Mutex::new(None);
586 let mut last = LAST.lock().unwrap();
587 if let Some((at, threads)) = *last
588 && at.elapsed() < THREADS_EVERY
589 {
590 return threads;
591 }
592 let others = match ®ISTRATION.lock().unwrap().registry {
593 Some(registry) => registry.others(),
594 None => ProcessRegistry::default_dir().map(|d| ProcessRegistry::others_in(&d)).unwrap_or_default(),
597 };
598 let busy = others.iter().filter(|o| o.busy.unwrap_or(o.idle < BUSY_WINDOW)).count();
599 let cpus = cpus();
600 let threads = auto_threads_for(cpus, busy);
601 *last = Some((Instant::now(), threads));
602 threads
603}
604
605pub(crate) fn cpus() -> usize {
607 std::thread::available_parallelism().map_or(8, |n| n.get())
608}
609
610const OVERSUBSCRIBE: usize = 4;
615
616pub fn auto_threads_for(cpus: usize, busy_others: usize) -> usize {
621 (cpus * OVERSUBSCRIBE / (busy_others + 1)).clamp(cpus.min(2), cpus)
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn threads_split_between_busy_processes() {
630 assert_eq!(auto_threads_for(32, 0), 32);
631 assert_eq!(auto_threads_for(32, 3), 32, "4 busy processes keep every CPU");
632 assert_eq!(auto_threads_for(32, 7), 16);
633 assert_eq!(auto_threads_for(32, 15), 8);
634 assert_eq!(auto_threads_for(32, 255), 2, "never below 2");
635 assert_eq!(auto_threads_for(1, 3), 1, "nor above the CPUs");
636 assert!(auto_threads() >= 1);
637 }
638
639 #[test]
640 fn workers_wait_for_their_turn() {
641 use crate::Threads;
642 Threads::Auto.wait_turn(0, || panic!("the first worker always runs"));
643 Threads::Fixed(1).wait_turn(5, || panic!("fixed counts never hold a worker back"));
644 let asked = std::sync::atomic::AtomicUsize::new(0);
645 Threads::Auto.wait_turn(Threads::Auto.most(), || asked.fetch_add(1, Ordering::Relaxed) >= 2);
647 assert_eq!(asked.load(Ordering::Relaxed), 3);
648 }
649
650 #[test]
651 fn results_roundtrip_and_errors_are_not_kept() {
652 let row = |rel| RefResult {
653 dex_name: "classes.dex".into(),
654 method_idx: 7,
655 method: "La;->b".into(),
656 signature: "La;->b()V".into(),
657 matched: vec![
658 Matched::String("x\ny".into()),
659 Matched::Symbol("La;->c:I".into()),
660 Matched::Field { symbol: "La;->f:I".into(), access: asc_findrefs::FieldAccess::Read },
661 ],
662 relation: rel,
663 sites: vec![
664 RefSite { offset: 9, matched: 1, field_access: None, pc: Some(3), line: None },
665 RefSite { offset: 11, matched: 2, field_access: Some(asc_findrefs::FieldAccess::Read), pc: Some(4), line: Some(8) },
666 ],
667 context: rel.map(|_| " const-string v0, \"x\"".to_string()),
668 };
669 let batch = |name: &str, results, error: Option<String>| RefBatch {
670 entry_name: name.into(),
671 results,
672 text: None,
673 inflate_us: 5,
674 search_us: 6,
675 source: "inflate",
676 error,
677 };
678 let mut rec = ResultRecorder::new(L3_MAX_RESULT);
679 rec.record(&batch("classes.dex", vec![row(None), row(Some(ClassRelation::Implements))], None));
680 rec.record(&batch("classes2.dex", vec![], None));
681 let bytes = rec.finish(&["a note".to_string()]).unwrap();
682 let Recorded { batches: back, notes } = decode_batches(bytes.clone()).unwrap();
683 assert_eq!(notes, ["a note"]);
684 assert_eq!(back.len(), 2);
685 assert_eq!(back[0].results, vec![row(None), row(Some(ClassRelation::Implements))]);
686 assert_eq!((back[1].entry_name.as_str(), back[1].source), ("classes2.dex", "result"));
687 assert!(decode_batches(bytes[..bytes.len() - 1].to_vec()).is_none());
688 let mut one = ResultRecorder::new(L3_MAX_RESULT);
690 one.record(&batch("classes2.dex", vec![], None));
691 let one = one.finish(&[]).unwrap();
692 assert!(decode_batches(one[..one.len() - 4].to_vec()).is_none());
693 assert_eq!(decode_batches(one).unwrap().batches.len(), 1);
694 assert!(decode_batches(Vec::new()).is_none());
695 let mut failed = ResultRecorder::new(L3_MAX_RESULT);
696 failed.record(&batch("classes.dex", vec![], Some("bad".into())));
697 assert!(failed.finish(&[]).is_none());
698 }
699
700 #[test]
701 fn text_rows_roundtrip_within_their_limit() {
702 let text = |lines: &[&str]| {
703 let bytes: Arc<dyn AsRef<[u8]> + Send + Sync> =
704 Arc::new(lines.iter().map(|l| format!("{l}\n")).collect::<String>().into_bytes());
705 let mut ends = Vec::new();
706 let mut at = 0;
707 for l in lines {
708 at += l.len();
709 ends.push(at);
710 at += 1;
711 }
712 let len = (*bytes).as_ref().len();
713 TextRows::shared(bytes, 0..len, ends).unwrap()
714 };
715 let batch = |name: &str, t: TextRows| RefBatch {
716 entry_name: name.into(),
717 results: Vec::new(),
718 text: Some(t),
719 inflate_us: 0,
720 search_us: 0,
721 source: "inflate",
722 error: None,
723 };
724 let a = text(&["classes.dex | La;->b()V | \"x\"", "classes.dex | La;->c()V | \"y\""]);
725 let mut rec = ResultRecorder::new(1 << 10);
726 rec.record(&batch("classes.dex", a.clone()));
727 rec.record(&batch("classes2.dex", text(&[])));
728 let Recorded { batches: back, notes } = decode_batches(rec.finish(&["n".to_string()]).unwrap()).unwrap();
729 assert_eq!(notes, ["n"]);
730 assert_eq!(back[0].text.as_ref(), Some(&a));
731 assert_eq!(back[0].row_count(), 2);
732 assert_eq!(back[1].text.as_ref().map(TextRows::len), Some(0));
733 let mut rec = ResultRecorder::new(64);
735 rec.record(&batch("classes.dex", a));
736 assert!(rec.out.is_empty() && rec.finish(&[]).is_none());
737 }
738}