use std::sync::Arc;
use std::thread;
use pedant_graph::{
CachedCodeGraph, CachedGraphAnalysis, DegreeCentrality, GraphAnalysis, GraphDirection,
GraphEdgeSelection, GraphLimits, GraphNodeId,
};
use super::analysis_fixture::{degree_texts, generous_limits, neighbor_texts};
use super::cache_analysis_reading::{first_node, readings};
use super::cache_counting::{
DERIVED_PRODUCT_EVICTIONS, DERIVED_PRODUCT_HITS, DERIVED_PRODUCT_MISSES,
SELECTED_INDEX_EVICTIONS, SELECTED_INDEX_HITS, SELECTED_INDEX_MISSES, assert_delta,
};
use super::cache_fixture::{cached_workspace, derived_only, generous};
use super::corpus::GRAPH_CORPUS;
const WORKERS: u64 = 4;
type WorkerReading = (Arc<[DegreeCentrality]>, Vec<(String, String)>);
pub fn assert_clear_eviction_and_drop_keep_live_handles_valid() {
assert_clearing_and_dropping_keep_held_values_valid();
assert_eviction_keeps_held_values_valid();
}
fn assert_clearing_and_dropping_keep_held_values_valid() {
let (resolved, mut cache, held) = cached_workspace(GRAPH_CORPUS, generous());
let selection = GraphEdgeSelection::all();
let analysis = held
.analyze(selection, generous_limits())
.expect("admitted");
let clone = analysis.clone();
let graph_clone = held.clone();
let seed = first_node(held.graph());
let neighbors = analysis
.neighbors(seed, 2, GraphDirection::Both)
.expect("answered");
let degrees = analysis.degree_centrality();
let expected = readings(&analysis, seed);
held.clear_analysis_cache();
assert_eq!(
readings(&clone, seed),
expected,
"clearing the derived state changes no answer"
);
cache.clear();
assert_eq!(
readings(&clone, seed),
expected,
"clearing the cache changes no answer"
);
let rebuilt = cache
.build_rust_graph(
&resolved.snapshot,
&resolved.resolution,
GraphLimits::default(),
)
.expect("the same claim rebuilds");
assert_eq!(
rebuilt.graph(),
graph_clone.graph(),
"a rebuilt graph equals the graph a caller still holds"
);
let cumulative = graph_clone.stats();
drop(cache);
drop(rebuilt);
assert_eq!(
readings(&clone, seed),
expected,
"dropping the cache changes no answer"
);
assert_eq!(
graph_clone.stats().exact_graph_misses(),
cumulative.exact_graph_misses(),
"cumulative statistics survive the cache that produced them"
);
assert_eq!(
neighbor_texts(graph_clone.graph(), &neighbors),
neighbor_texts(
clone.graph(),
&clone
.neighbors(seed, 2, GraphDirection::Both)
.expect("answered")
),
"a product held across clear and drop still states its own answer"
);
let direct = GraphAnalysis::new(graph_clone.graph(), selection, generous_limits())
.expect("the direct analysis builds over the graph a caller still holds");
assert_eq!(
degree_texts(graph_clone.graph(), &direct.degree_centrality()),
degree_texts(graph_clone.graph(), °rees),
"a metric held across clear and drop still states the direct metric"
);
}
fn assert_eviction_keeps_held_values_valid() {
let (_resolved, _cache, held) = cached_workspace(GRAPH_CORPUS, derived_only(1, 1));
let selection = GraphEdgeSelection::all();
let analysis = held
.analyze(selection, generous_limits())
.expect("admitted");
let seed = first_node(held.graph());
let retained = held.stats();
let neighbors = analysis
.neighbors(seed, 2, GraphDirection::Both)
.expect("answered");
let degrees = analysis.degree_centrality();
assert_delta(
retained,
held.stats(),
&[(DERIVED_PRODUCT_MISSES, 2), (DERIVED_PRODUCT_EVICTIONS, 1)],
"a second product displaces the neighborhood a caller still holds",
);
let indexed = held.stats();
held.analyze(GraphEdgeSelection::code_relations(), generous_limits())
.expect("a second selection is admitted");
assert_delta(
indexed,
held.stats(),
&[(SELECTED_INDEX_MISSES, 1), (SELECTED_INDEX_EVICTIONS, 1)],
"a second selection displaces the index set a caller still holds",
);
let direct = GraphAnalysis::new(held.graph(), selection, generous_limits())
.expect("the direct analysis builds");
assert_eq!(
readings(&analysis, seed),
readings(&direct, seed),
"an analysis held across the eviction of its index set answers as the direct analysis does"
);
assert_eq!(
neighbor_texts(held.graph(), &neighbors),
neighbor_texts(
held.graph(),
&direct
.neighbors(seed, 2, GraphDirection::Both)
.expect("answered")
),
"a product held across its own eviction still states its answer"
);
assert_eq!(
degree_texts(held.graph(), &direct.degree_centrality()),
degree_texts(held.graph(), °rees),
"a metric held across a later eviction still states the direct metric"
);
}
pub fn assert_cached_analysis_is_thread_shareable_and_deterministic() {
let (_resolved, _cache, held) = cached_workspace(GRAPH_CORPUS, generous());
let selection = GraphEdgeSelection::all();
let analysis = held
.analyze(selection, generous_limits())
.expect("admitted");
let seed = first_node(held.graph());
let direct = GraphAnalysis::new(held.graph(), selection, generous_limits())
.expect("the direct analysis builds");
let expected = readings(&direct, seed);
let claims = claimed(expected.len());
let before = held.stats();
let answers = read_from_threads(&held, &analysis, (selection, seed));
assert_delta(
before,
held.stats(),
&[
(SELECTED_INDEX_HITS, WORKERS),
(DERIVED_PRODUCT_MISSES, claims),
(DERIVED_PRODUCT_HITS, WORKERS * claims + WORKERS - claims),
],
"every claim several threads ask for is derived exactly once",
);
let settled = analysis.degree_centrality();
for (metric, answer) in &answers {
assert_eq!(
answer, &expected,
"every thread reads the same derived answers"
);
assert!(
Arc::ptr_eq(metric, &settled),
"every thread answered with the one allocation the store retained"
);
}
assert_eq!(
degree_texts(held.graph(), &direct.degree_centrality()),
degree_texts(held.graph(), &settled),
"the shared metric equals the direct one"
);
}
fn read_from_threads(
held: &CachedCodeGraph,
analysis: &CachedGraphAnalysis,
asked: (GraphEdgeSelection, GraphNodeId),
) -> Vec<WorkerReading> {
let (selection, seed) = asked;
thread::scope(|scope| {
let workers: Vec<_> = (0..WORKERS)
.map(|_| {
let handle = analysis.clone();
let graph = held.clone();
scope.spawn(move || {
let reread = graph
.analyze(selection, generous_limits())
.expect("a cloned graph admits the same analysis");
(reread.degree_centrality(), readings(&handle, seed))
})
})
.collect();
workers
.into_iter()
.map(|worker| worker.join().expect("every worker finishes"))
.collect()
})
}
fn claimed(rows: usize) -> u64 {
u64::try_from(rows - 1).expect("a reading states fewer rows than u64 can count")
}