88#include < cassert>
99#include < cstddef>
1010#include < memory>
11+ #include < stack>
1112#include < string>
1213#include < unordered_set>
1314#include < utility>
@@ -59,15 +60,15 @@ MoveCommitter::MoveCommitter(Resizer& resizer) : resizer_(resizer)
5960
6061void MoveCommitter::init ()
6162{
62- journal_open_ = false ;
63+ pending_move_results_ = std::stack<std::vector<MoveResult>>{} ;
6364 std::ranges::fill (summary_carried_by_type_, 0 );
6465 summary_carried_by_type_[typeIndex (MoveType::kSizeUpMatch )]
6566 = persisted_summary_by_type_[typeIndex (MoveType::kSizeUpMatch )];
6667 std::ranges::fill (pending_by_type_, 0 );
6768 std::ranges::fill (committed_by_type_, 0 );
6869 for (size_t index = 0 ; index < pending_instances_by_type_.size (); ++index) {
6970 pending_instances_by_type_[index].clear ();
70- all_instances_by_type_ [index].clear ();
71+ committed_instances_by_type_ [index].clear ();
7172 }
7273
7374 const bool report_enabled
@@ -90,6 +91,12 @@ MoveResult MoveCommitter::commit(MoveCandidate& candidate)
9091 MoveResult result = candidate.apply ();
9192 if (result.accepted ) {
9293 recordAcceptedResult (result);
94+ // Only the journaled path needs the per-level result trail; non-journaled
95+ // commits (e.g. MeasuredVtSwapPolicy) flush via acceptPendingMoves and
96+ // never restore.
97+ if (!pending_move_results_.empty ()) {
98+ pending_move_results_.top ().push_back (result);
99+ }
93100 }
94101 return result;
95102}
@@ -366,16 +373,28 @@ bool MoveCommitter::canManageJournal() const
366373
367374void MoveCommitter::recordAcceptedResult (const MoveResult& result)
368375{
369- // Track committed instances by move type so later passes can avoid
370- // conflicts.
376+ // Track committed instances by move type so later passes can avoid conflicts
371377 const size_t index = typeIndex (result.type );
372378 pending_by_type_[index] += result.move_count ;
373379 for (sta::Instance* inst : result.touched_instances ) {
374380 pending_instances_by_type_[index].insert (inst);
375381 // Preserve legacy BaseMove semantics: once a move type touched an
376382 // instance in this repair session, later guards still see it even if the
377383 // journal branch is restored.
378- all_instances_by_type_[index].insert (inst);
384+ // FIXME: This should be removed after policies start using the nested
385+ // journaling capability.
386+ committed_instances_by_type_[index].insert (inst);
387+ }
388+ }
389+
390+ void MoveCommitter::unrecordAcceptedResult (const MoveResult& result)
391+ {
392+ // Remove the reverted move results
393+ const size_t index = typeIndex (result.type );
394+ pending_by_type_[index] -= result.move_count ;
395+ for (sta::Instance* inst : result.touched_instances ) {
396+ pending_instances_by_type_[index].erase (
397+ pending_instances_by_type_[index].find (inst));
379398 }
380399}
381400
@@ -392,16 +411,13 @@ void MoveCommitter::beginEcoJournal() const
392411
393412void MoveCommitter::commitEcoJournal () const
394413{
395- debugPrint (resizer_.logger (), RSZ , " journal" , 1 , " journal end" );
396- // Refresh timing only once after a batch of ECO edits becomes permanent.
397- if (ecoHasPendingChanges ()) {
398- resizer_.updateParasiticsAndTiming ();
399- }
414+ debugPrint (resizer_.logger (), RSZ , " journal" , 1 , " journal commit" );
400415 odb::dbDatabase::commitEco (resizer_.block ());
401416}
402417
403418void MoveCommitter::restoreEcoJournal () const
404419{
420+ debugPrint (resizer_.logger (), RSZ , " journal" , 1 , " journal restore" );
405421 odb::dbDatabase::undoEco (resizer_.block ());
406422}
407423
@@ -466,51 +482,115 @@ void MoveCommitter::beginJournal()
466482 if (!canManageJournal ()) {
467483 return ;
468484 }
469-
470- // Start one reversible ECO batch for the current search branch.
485+ // Start one reversible ECO batch for the current search branch. Stacks
486+ // naturally: odb's beginEco pushes the current journal aside, and we mirror
487+ // that with a fresh per-level result vector here.
471488 beginEcoJournal ();
472- journal_open_ = true ;
489+ pending_move_results_.emplace ();
490+ // Mirror the journal level in the tracker so a nested restore rejects only
491+ // its own moves.
492+ if (move_tracker_) {
493+ move_tracker_->beginJournal ();
494+ }
473495}
474496
475497void MoveCommitter::commitJournal ()
476498{
477- if (!canManageJournal () || !journal_open_ ) {
499+ if (!canManageJournal () || pending_move_results_. empty () ) {
478500 return ;
479501 }
480-
481- // Close the reversible batch and fold the accepted moves into the totals.
502+ // Refresh timing only once after a batch of ECO edits becomes permanent
503+ if (ecoHasPendingChanges ()) {
504+ resizer_.updateParasiticsAndTiming ();
505+ }
506+ // Close the reversible batch. odb::commitEco appends the inner journal to its
507+ // parent when one exists, otherwise the changes become permanent.
482508 commitEcoJournal ();
483- acceptPendingMoves ();
484- journal_open_ = false ;
509+ // Mirror the journal commit in the tracker: an outermost commit finalizes
510+ // the tracked moves, a nested commit merges them into the parent level.
511+ if (move_tracker_) {
512+ move_tracker_->commitJournal ();
513+ }
514+
515+ std::vector<MoveResult> top = std::move (pending_move_results_.top ());
516+ pending_move_results_.pop ();
517+
518+ if (pending_move_results_.empty ()) {
519+ // Outermost commit: fold the accumulated pending counters into the
520+ // committed totals and flush the tracker.
521+ acceptPendingMoves ();
522+ } else {
523+ // Nested commit: the moves remain pending against the parent journal,
524+ // so the parent restore can still undo them. Merge our results into
525+ // the parent's vector; pending_by_type_ / pending_instances_by_type_
526+ // already reflect the cumulative state and stay untouched.
527+ debugPrint (resizer_.logger (),
528+ RSZ ,
529+ " journal" ,
530+ 2 ,
531+ " nested journal commit: merged {} results into parent" ,
532+ top.size ());
533+ auto & parent = pending_move_results_.top ();
534+ for (MoveResult& result : top) {
535+ parent.push_back (std::move (result));
536+ }
537+ }
485538}
486539
487540void MoveCommitter::restoreJournal ()
488541{
489- if (!canManageJournal () || !journal_open_ ) {
542+ if (!canManageJournal () || pending_move_results_. empty () ) {
490543 return ;
491544 }
492-
493- // Undo both database edits and in-memory accounting for the rejected branch.
545+ // Undo both database edits and in-memory accounting for the rejected branch
494546 debugPrint (
495547 resizer_.logger (), RSZ , " journal" , 1 , " journal restore starts >>>" );
496548 resizer_.initForJournalRestore ();
497549
498550 const bool had_eco_changes = ecoHasPendingChanges ();
499551 restoreEcoJournal ();
500- if (!had_eco_changes) {
501- rejectPendingMoves ();
502- journal_open_ = false ;
552+ // Mirror the journal restore in the tracker so its moves are rejected,
553+ // whether this is an outermost or a nested restore.
554+ if (move_tracker_) {
555+ move_tracker_->restoreJournal ();
556+ }
557+
558+ // Unrecord just this level's accepted results; the parent's accumulated
559+ // pending state (if any) must survive.
560+ std::vector<MoveResult> top = std::move (pending_move_results_.top ());
561+ pending_move_results_.pop ();
562+ int reverted_moves = 0 ;
563+ for (const MoveResult& result : top) {
564+ reverted_moves += result.move_count ;
565+ }
566+ resizer_.addRejectedLegacyMoveCount (reverted_moves);
567+ if (pending_move_results_.empty ()) {
568+ // pending_by_type_ still holds the about-to-be-reverted counts at this
569+ // point; log the per-type breakdown before draining them.
570+ logPendingMoves (" UNDO" , reverted_moves);
571+ } else {
503572 debugPrint (resizer_.logger (),
504573 RSZ ,
505574 " journal" ,
506- 1 ,
507- " journal restore ends due to empty ECO >>>" );
508- return ;
575+ 2 ,
576+ " nested journal restore: reverted {} moves" ,
577+ reverted_moves);
578+ }
579+ for (const MoveResult& result : top) {
580+ unrecordAcceptedResult (result);
509581 }
510582
511- resizer_.updateParasiticsAndTiming ();
512- rejectPendingMoves ();
513- journal_open_ = false ;
583+ if (had_eco_changes) {
584+ resizer_.updateParasiticsAndTiming ();
585+ resizer_.invalidateVertexOrdering ();
586+ }
587+
588+ if (pending_move_results_.empty ()) {
589+ // Outermost restore: the tracker's pending entries are no longer
590+ // load-bearing. Flush them and emit the totals snapshot.
591+ rejectTrackedMoves ();
592+ logCommittedTotals ();
593+ }
514594
515595 debugPrint (resizer_.logger (), RSZ , " journal" , 1 , " journal restore ends <<<" );
516596}
@@ -527,7 +607,7 @@ void MoveCommitter::acceptPendingMoves()
527607 }
528608 committed_by_type_[index] += pending_by_type_[index];
529609 pending_by_type_[index] = 0 ;
530- all_instances_by_type_ [index].insert (
610+ committed_instances_by_type_ [index].insert (
531611 pending_instances_by_type_[index].begin (),
532612 pending_instances_by_type_[index].end ());
533613 pending_instances_by_type_[index].clear ();
@@ -544,7 +624,7 @@ void MoveCommitter::rejectPendingMoves()
544624 resizer_.addRejectedLegacyMoveCount (move_count);
545625
546626 std::ranges::fill (pending_by_type_, 0 );
547- for (std::unordered_set <sta::Instance*>& pending_instances :
627+ for (std::unordered_multiset <sta::Instance*>& pending_instances :
548628 pending_instances_by_type_) {
549629 pending_instances.clear ();
550630 }
@@ -585,7 +665,7 @@ bool MoveCommitter::hasPendingMoves(const MoveType type,
585665bool MoveCommitter::hasMoves (const MoveType type, sta::Instance* inst) const
586666{
587667 const size_t index = typeIndex (type);
588- return all_instances_by_type_ [index].contains (inst)
668+ return committed_instances_by_type_ [index].contains (inst)
589669 || pending_instances_by_type_[index].contains (inst);
590670}
591671
0 commit comments