Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions src/gitmine.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <optional> // CoCouplingCache — the lazily-built StaticIncludeCoupling shared across probes
#include <mutex> // gitRepoToplevel's per-directory memo — one rev-parse probe per root, not per miner
#include <string>
#include <string_view>
Expand Down Expand Up @@ -2692,6 +2693,32 @@ class StaticIncludeCoupling
mutable HashMap<std::uint32_t, std::vector<char>> memo_;
};

// One StaticIncludeCoupling per INGEST, built on first need and shared by every cochangePartners probe of
// that ingest (prcontext-hoist, CHECKLIST 30(a)). The coupling is a pure function of `ing` alone (its
// include list + buildPreciseIncludeAdj), so a multi-probe caller (--pr-context, --situ) rebuilt the SAME
// whole-ingest include graph once per changed file — ~87% of a --pr-context=HEAD~30 run. The key is the
// ingest itself, held by reference: the cache cannot answer for another one (EXPECTS in get), and it dies
// with the caller's scope, before `ing` does. Lazy, so a probe that never clears kSupport (no git, a clean
// tree) still builds nothing — exactly the per-call cost it had before.
class CoCouplingCache
{
public:
explicit CoCouplingCache( const IngestResult& ing ) : ing_( ing ) {}
const StaticIncludeCoupling& get( const IngestResult& ing )
{
EXPECTS( &ing == &ing_, "a CoCouplingCache answers only for the ingest it was built for" );
if( !coupling_ )
{
coupling_.emplace( ing_ );
}
return *coupling_;
}

private:
const IngestResult& ing_;
std::optional<StaticIncludeCoupling> coupling_;
};

// ─── recurrence across sub-windows (Clio, ICSE 2011) ──────────────────────────────────────────────
//
// THE DEFECT THIS CLOSES. `--cochange` mined ONE window (18 months by default) and reported any pair
Expand Down Expand Up @@ -2907,7 +2934,8 @@ inline const char* coPairAttr( const CoPartner& p ) noexcept { return coPairAttr
// `outSubWindows` (optional): the recurrence denominator this call actually used, for the emitter's
// `sub_windows=` disclosure. Nobody may publish recur= without it — see kCoRecurSubWindows above.
inline std::vector<CoPartner> cochangePartners( const IngestResult& ing, std::string_view fileSubstr, std::uint32_t& outCommits,
const std::vector<std::vector<std::uint32_t>>& sets, std::uint32_t* outSubWindows = nullptr )
const std::vector<std::vector<std::uint32_t>>& sets, std::uint32_t* outSubWindows = nullptr,
CoCouplingCache* couplingCache = nullptr ) // multi-probe callers share one (see CoCouplingCache)
{
outCommits = 0;
if( outSubWindows )
Expand Down Expand Up @@ -2959,7 +2987,9 @@ inline std::vector<CoPartner> cochangePartners( const IngestResult& ing, std::st
// A4-F22 / P9.1: "surprising" must mean "no static #include coupling *either way, transitively*" —
// see StaticIncludeCoupling above, the ONE predicate shared with --cochange's repo-wide pair scan
// in main.cpp (P9.1 was the two paths disagreeing).
const StaticIncludeCoupling coupling( ing );
// Built at most once per ingest when the caller passes `couplingCache`; a one-off local cache otherwise.
std::optional<CoCouplingCache> localCache;
CoCouplingCache& cache = couplingCache ? *couplingCache : localCache.emplace( ing );
constexpr std::uint32_t kSupport = 3;
for( const auto& [other, n] : together )
{
Expand All @@ -2970,7 +3000,7 @@ inline std::vector<CoPartner> cochangePartners( const IngestResult& ing, std::st
const auto maskIt = recurMask.find( other );
res.push_back( { other, n, coConfidence( n, freq[fid] ), coConfidence( n, freq[other] ),
coRecurrenceOf( maskIt == recurMask.end() ? 0u : maskIt->second ),
isDepCapable && !coupling.isStaticallyCoupled( fid, other ), isDepCapable } );
isDepCapable && !cache.get( ing ).isStaticallyCoupled( fid, other ), isDepCapable } );
}
}
std::sort( res.begin(), res.end(), [ & ]( const CoPartner& x, const CoPartner& y )
Expand Down
3 changes: 2 additions & 1 deletion src/prcontext.h
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,7 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest
// file — unbounded storm). Deterministic for a fixed HEAD, so every per-file probe below is identical.
// Mined ONCE here even when the budget forces several render passes below (each pass is pure re-emission).
const auto coSets = gitCommitFileSets( root, ing, "18 months ago", 30, nullptr, onlyRoot );
CoCouplingCache coCoupling( ing ); // prcontext-hoist: ONE include-coupling build for every file and every render pass
const std::string coWindow = defaultWindowLabel( root, "18mo" ); // F2: <cochange commits=> had no window to read it against

// A4-P?: mine file OWNERSHIP ONCE too (was gitFileAuthors(root, ing, f) — one `git log` popen PER changed
Expand Down Expand Up @@ -1278,7 +1279,7 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest
// (6) co-change partners NOT in the diff (gitmine). Degrades to an empty list without git. A4-P10:
// answered from the once-mined `coSets` (no per-file `git log` popen).
std::uint32_t commits = 0;
const std::vector<CoPartner> partners = cochangePartners( ing, ing.files[f], commits, coSets );
const std::vector<CoPartner> partners = cochangePartners( ing, ing.files[f], commits, coSets, nullptr, &coCoupling );
std::vector<const CoPartner*> outside;
for( const CoPartner& p : partners )
{
Expand Down
6 changes: 4 additions & 2 deletions src/situ.h
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,7 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges
// A4-P10: mine the commit file-sets ONCE (not one `git log` popen per probed file) and answer every probe
// from the shared sets — identical result (mining is deterministic for a fixed HEAD), no subprocess storm.
const auto coSets = gitCommitFileSets( root, ing, "18 months ago", 30, nullptr, onlyRoot );
CoCouplingCache coCoupling( ing ); // prcontext-hoist: one include-coupling build for every probe
const std::string coWindow = defaultWindowLabel( root, "18mo" ); // F2: see SituationFacts::coWindow
const std::size_t coCommits = coSets.size();
HashMap<std::uint32_t, double> partnerDeg;
Expand All @@ -852,7 +853,7 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges
}
++probed;
std::uint32_t commits = 0;
const std::vector<CoPartner> ps = cochangePartners( ing, ing.files[f], commits, coSets );
const std::vector<CoPartner> ps = cochangePartners( ing, ing.files[f], commits, coSets, nullptr, &coCoupling );
for( const CoPartner& p : ps )
{
if( !changedFile[p.fileId] )
Expand Down Expand Up @@ -1007,6 +1008,7 @@ inline SituationFacts computeSituationFacts( const std::string& root, const Inge
// A4-P10: mine the commit file-sets ONCE and answer every probe from the shared sets (was one `git log`
// popen per probed file — up to 40 — the O(files)-subprocess storm). Deterministic for a fixed HEAD.
const auto coSets = gitCommitFileSets( root, ing, "18 months ago", 30 );
CoCouplingCache coCoupling( ing ); // prcontext-hoist: one include-coupling build for every probe
facts.declDef = declDefPartners( ing, changedFile ); // F3: same relationship, same rule, one implementation
facts.siblings = lexicalSiblings( ing, changedFile ); // L-D: the same list the CLI report's [1] prints
facts.coWindow = defaultWindowLabel( root, "18mo" ); // F2: the composed zero's window travels WITH the zero
Expand All @@ -1022,7 +1024,7 @@ inline SituationFacts computeSituationFacts( const std::string& root, const Inge
}
++probed;
std::uint32_t commits = 0;
const std::vector<CoPartner> ps = cochangePartners( ing, ing.files[f], commits, coSets );
const std::vector<CoPartner> ps = cochangePartners( ing, ing.files[f], commits, coSets, nullptr, &coCoupling );
for( const CoPartner& p : ps )
{
if( !changedFile[p.fileId] )
Expand Down
Loading