Skip to content

Commit cf3fb22

Browse files
committed
Merge remote-tracking branch 'origin/master' into sta-debug
2 parents 5066230 + 62074e2 commit cf3fb22

18 files changed

Lines changed: 353 additions & 70 deletions

File tree

src/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,6 @@ target_link_libraries(openroad
345345
cut
346346
grt
347347
tap
348-
gui
349348
drt
350349
dst
351350
psm
@@ -358,6 +357,7 @@ target_link_libraries(openroad
358357
par
359358
est
360359
web
360+
gui
361361
absl::synchronization
362362
${ABC_LIBRARY}
363363
${TCL_LIBRARY}

src/gpl/src/nesterovBase.cpp

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -839,26 +839,22 @@ void BinGrid::updateBinsNonPlaceArea()
839839
using boost::polygon::operators::operator+=;
840840
using boost::polygon::operators::operator&=;
841841

842-
// Build the union of all non-place instance rectangles. Overlapping
843-
// fixed macros (or blockages) would otherwise be double-counted into
844-
// the bins they share, pushing density above 100% and producing
845-
// artificially large repulsive forces on movable cells.
846-
Polygon90Set fixed_set;
847-
for (auto& inst : pb_->nonPlaceInsts()) {
842+
// For each bin, collect indices of non-place instances whose bbox
843+
// overlaps it. The per-bin geometric union (which dedupes overlapping
844+
// fixed macros / blockages) only depends on those instances, so we
845+
// avoid copying a full design-wide polygon set per bin.
846+
const auto& non_place_insts = pb_->nonPlaceInsts();
847+
std::vector<std::vector<int>> bin_insts(bins_.size());
848+
for (size_t i = 0; i < non_place_insts.size(); ++i) {
849+
const Instance* inst = non_place_insts[i];
848850
if (inst->lx() >= inst->ux() || inst->ly() >= inst->uy()) {
849851
continue;
850852
}
851-
fixed_set += BoostRect(inst->lx(), inst->ly(), inst->ux(), inst->uy());
852-
}
853-
854-
// Identify bins touched by any fixed instance to skip the empty ones.
855-
std::vector<bool> touched(bins_.size(), false);
856-
for (auto& inst : pb_->nonPlaceInsts()) {
857853
std::pair<int, int> pairX = getMinMaxIdxX(inst);
858854
std::pair<int, int> pairY = getMinMaxIdxY(inst);
859855
for (int y = pairY.first; y < pairY.second; y++) {
860856
for (int x = pairX.first; x < pairX.second; x++) {
861-
touched[y * binCntX_ + x] = true;
857+
bin_insts[y * binCntX_ + x].push_back(static_cast<int>(i));
862858
}
863859
}
864860
}
@@ -867,13 +863,31 @@ void BinGrid::updateBinsNonPlaceArea()
867863
// fixed instances). Drives nonPlaceAreaUnscaled and the cap below.
868864
std::vector<int64_t> unionArea(bins_.size(), 0);
869865
for (size_t i = 0; i < bins_.size(); ++i) {
870-
if (!touched[i]) {
866+
const auto& touching = bin_insts[i];
867+
if (touching.empty()) {
871868
continue;
872869
}
873870
Bin& bin = bins_[i];
874-
Polygon90Set clip = fixed_set;
875-
clip &= BoostRect(bin.lx(), bin.ly(), bin.ux(), bin.uy());
876-
unionArea[i] = boost::polygon::area(clip);
871+
if (touching.size() == 1) {
872+
// Single instance: union == clipped overlap, skip Boost.Polygon.
873+
const Instance* inst = non_place_insts[touching.front()];
874+
const int rectLx = std::max(bin.lx(), inst->lx());
875+
const int rectLy = std::max(bin.ly(), inst->ly());
876+
const int rectUx = std::min(bin.ux(), inst->ux());
877+
const int rectUy = std::min(bin.uy(), inst->uy());
878+
if (rectLx < rectUx && rectLy < rectUy) {
879+
unionArea[i] = static_cast<int64_t>(rectUx - rectLx)
880+
* static_cast<int64_t>(rectUy - rectLy);
881+
}
882+
} else {
883+
Polygon90Set local_set;
884+
for (int idx : touching) {
885+
const Instance* inst = non_place_insts[idx];
886+
local_set += BoostRect(inst->lx(), inst->ly(), inst->ux(), inst->uy());
887+
}
888+
local_set &= BoostRect(bin.lx(), bin.ly(), bin.ux(), bin.uy());
889+
unionArea[i] = boost::polygon::area(local_set);
890+
}
877891
// Note that nonPlaceArea should have scale-down with target
878892
// density. See MS-replace paper.
879893
bin.setNonPlaceAreaUnscaled(
@@ -899,7 +913,7 @@ void BinGrid::updateBinsNonPlaceArea()
899913
}
900914
}
901915
for (size_t i = 0; i < bins_.size(); ++i) {
902-
if (!touched[i]) {
916+
if (bin_insts[i].empty()) {
903917
continue;
904918
}
905919
Bin& bin = bins_[i];

src/grt/src/Rudy.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "grt/Rudy.h"
55

66
#include <algorithm>
7+
#include <climits>
78
#include <cstdint>
89
#include <optional>
910
#include <set>
@@ -147,6 +148,9 @@ void Rudy::processNet(odb::dbNet* net)
147148

148149
void Rudy::processIntersectionSignalNet(const odb::Rect net_rect)
149150
{
151+
if (net_rect.isInverted()) {
152+
return;
153+
}
150154
const auto net_area = net_rect.area();
151155
if (net_area == 0) {
152156
// TODO: handle nets with 0 area from getTermBBox()

src/grt/test/save_guideok

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
set -e
77

8+
script_dir="$(dirname "$(readlink -f "$0")")/../../../test/shared"
9+
bazel_pending=()
10+
811
for test_name in "${@:1}"
912
do
1013
if [ -f "results/${test_name}-tcl.guide" ]; then
@@ -14,6 +17,10 @@ do
1417
cp "results/${test_name}-py.guide" "${test_name}.guideok"
1518
echo "${test_name}"
1619
else
17-
echo "\"${test_name}\" guide file not found"
20+
bazel_pending+=("${test_name}")
1821
fi
1922
done
23+
24+
if [ ${#bazel_pending[@]} -gt 0 ]; then
25+
"${script_dir}/bazel_save.sh" guideok guide "${bazel_pending[@]}"
26+
fi

src/web/include/web/web.h

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#pragma once
55

6+
#include <spdlog/common.h>
7+
68
#include <condition_variable>
79
#include <cstdint>
810
#include <functional>
@@ -108,29 +110,37 @@ class WebServer
108110
double dbu_per_pixel,
109111
const std::string& vis_json);
110112

111-
private:
112-
// Tears down the I/O threads and cleans up hooks. Called by the
113-
// destructor; safe to call multiple times.
113+
// Tears down the I/O threads and cleans up hooks. Safe to call multiple
114+
// times and from any thread; after it returns, isRunning() is false and
115+
// serve() may be called again to restart the server.
114116
void stop();
115117

118+
private:
119+
// Stops ioc_, joins every worker thread except the current one, and
120+
// clears threads_. Detaches the current thread if it happens to be a
121+
// worker (would otherwise raise EDEADLK on self-join).
122+
void stopAndJoinIoThreads();
123+
116124
odb::dbDatabase* db_ = nullptr;
117125
sta::dbSta* sta_ = nullptr;
118126
utl::Logger* logger_ = nullptr;
119127
Tcl_Interp* interp_ = nullptr;
120128
int num_threads_ = 0;
121129
std::shared_ptr<TileGenerator> generator_;
122130
std::unique_ptr<WebViewerHook> viewer_hook_;
123-
std::shared_ptr<spdlog::sinks::sink> log_sink_;
124131

125132
// Background I/O context and worker threads (non-null while running).
126133
std::unique_ptr<boost::asio::io_context> ioc_;
127134
std::vector<std::thread> threads_;
128135

129-
// Type-erased callback that closes the Listener's acceptor before the
130-
// io_context is destroyed — prevents a crash where the acceptor's
131-
// destructor references the io_context that's mid-destruction.
136+
// Closes the Listener's acceptor before the io_context is destroyed,
137+
// avoiding a crash where the acceptor references a half-destroyed
138+
// io_context.
132139
std::function<void()> shutdown_listener_;
133140

141+
// Held so stop() can remove it from the Logger before viewer_hook_ is
142+
// destroyed — the sink stores a raw pointer into the hook.
143+
spdlog::sink_ptr log_sink_;
134144
// Blocking support: waitForStop() sleeps on stop_cv_ until
135145
// requestStop() sets stop_requested_.
136146
std::mutex stop_mutex_;

src/web/src/main.js

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,42 @@ function tclAppend(text, className) {
361361
app.tclOutputEl.scrollTop = app.tclOutputEl.scrollHeight;
362362
}
363363

364+
// Browser UX for `exit`/`quit` typed in the Tcl console. The browser
365+
// override (web_serve.cpp tclExitHandler) sets exit_requested_, and
366+
// Main.cc calls exit(EXIT_SUCCESS) once waitForStop() returns — so the
367+
// whole OpenROAD process exits, not just the web session. (Compare
368+
// `web_server -stop`, which only stops serving and arrives here as a
369+
// broadcast `type: shutdown` handled below.)
370+
// window.close() only succeeds when the tab was opened via JS (or via
371+
// certain launcher integrations); when it fails we replace the page
372+
// with a terminal overlay so the user knows OpenROAD exited and they
373+
// can close the tab manually.
374+
function handleServerShutdown() {
375+
// Idempotent: invoked from both the Tcl-eval response (`action: shutdown`)
376+
// and the broadcast push (`type: shutdown`); whichever arrives first wins.
377+
if (app._shutdownHandled) return;
378+
app._shutdownHandled = true;
379+
// Disable auto-reconnect and suppress the "disconnected" banner —
380+
// the disconnect is intentional.
381+
if (app.websocketManager) {
382+
app.websocketManager._shutdown = true;
383+
app.websocketManager.onPush = () => {};
384+
}
385+
const overlay = document.createElement('div');
386+
overlay.style.cssText =
387+
'position:fixed;inset:0;z-index:99999;background:#1e1e1e;color:#ddd;' +
388+
'display:flex;flex-direction:column;align-items:center;justify-content:center;' +
389+
'font-family:system-ui,sans-serif;font-size:16px;padding:24px;text-align:center;';
390+
overlay.innerHTML =
391+
'<div style="font-size:22px;margin-bottom:12px;">OpenROAD exited</div>' +
392+
'<div style="opacity:0.7;">You can close this tab.</div>';
393+
document.body.appendChild(overlay);
394+
// Hold the overlay visible long enough for the user to read it before
395+
// window.close() fires. 400 ms was below the perceptual threshold and
396+
// looked like the tab vanished instantly on `exit`.
397+
setTimeout(() => { try { window.close(); } catch (e) { /* ignore */ } }, 1500);
398+
}
399+
364400
function createTclConsole(container) {
365401
const el = document.createElement('div');
366402
el.className = 'tcl-console';
@@ -396,6 +432,9 @@ function createTclConsole(container) {
396432
tclAppend(data.result + '\n',
397433
data.is_error ? 'tcl-error' : '');
398434
}
435+
if (data.action === 'shutdown') {
436+
handleServerShutdown();
437+
}
399438
})
400439
.catch(err => tclAppend(`Error: ${err}\n`, 'tcl-error'));
401440
}
@@ -730,7 +769,11 @@ app.websocketManager.onPush = (msg) => {
730769
if (text) tclAppend(text + '\n', '');
731770
} else if (msg.type === 'shutdown') {
732771
// Server is stopping intentionally (web_server -stop).
733-
// Disable auto-reconnect and show a clear message.
772+
// Disable auto-reconnect and show a clear message. Note that
773+
// when the user typed `exit`/`quit` in the browser, the eval
774+
// response's `action: shutdown` already ran handleServerShutdown
775+
// (which set _shutdown and replaced onPush with a no-op), so
776+
// this branch only runs in the external-stop case.
734777
app.websocketManager._shutdown = true;
735778
statusDiv.innerHTML = '<div class="disconnected-banner">Server stopped</div>';
736779
statusDiv.style.display = 'block';

src/web/src/request_handler.cpp

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "request_dispatcher.h"
3838
#include "tile_generator.h"
3939
#include "timing_report.h"
40+
#include "utl/Logger.h"
4041

4142
namespace web {
4243

@@ -1418,12 +1419,27 @@ WebSocketResponse TclHandler::handleTclEval(const WebSocketRequest& req)
14181419
resp.id = req.id;
14191420
resp.type = WebSocketResponse::kJson;
14201421
try {
1421-
auto result = tcl_eval_->eval(extract_string(req.raw_json, "cmd"));
1422+
const std::string cmd = extract_string(req.raw_json, "cmd");
1423+
auto result = tcl_eval_->eval(cmd);
1424+
// tclExitHandler (web_serve.cpp) sets this sentinel as the Tcl
1425+
// result whenever `exit`/`quit` is evaluated through the override —
1426+
// whether typed bare in the browser or buried in `eval`/`source`.
1427+
// Convert it to a clean shutdown signal for the browser; the actual
1428+
// teardown is already requested by tclExitHandler via requestStop().
1429+
const bool is_exit = (result.result == kExitResultMsg);
14221430
JsonBuilder builder;
14231431
builder.beginObject();
14241432
builder.field("output", result.output);
1425-
builder.field("result", result.result);
1426-
builder.field("is_error", result.is_error);
1433+
if (is_exit) {
1434+
tcl_eval_->logger->info(
1435+
utl::WEB, 40, "Exit requested from web GUI; shutting down.");
1436+
builder.field("result", "Exiting OpenROAD.");
1437+
builder.field("is_error", false);
1438+
builder.field("action", "shutdown");
1439+
} else {
1440+
builder.field("result", result.result);
1441+
builder.field("is_error", result.is_error);
1442+
}
14271443
builder.endObject();
14281444
const std::string& json = builder.str();
14291445
resp.payload.assign(json.begin(), json.end());

src/web/src/request_handler.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ class RequestDispatcher;
2626
class TimingReport;
2727
class ClockTreeReport;
2828

29+
// Sentinel string set as the Tcl result by WebServer::tclExitHandler
30+
// when the browser-side Tcl `exit`/`quit` is invoked. TclHandler
31+
// detects this in handleTclEval and converts the response to a clean
32+
// shutdown signal for the browser.
33+
inline constexpr const char* kExitResultMsg = "_WEB_EXITING_";
34+
2935
// Thread-safe Tcl command evaluation with output capture.
3036
struct TclEvaluator
3137
{

src/web/src/web.cpp

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,31 @@ WebServer::WebServer(odb::dbDatabase* db,
813813
{
814814
}
815815

816+
// Defined here (not in web_serve.cpp) so the destructor's TU does not
817+
// pull in web_serve.cpp's gui::Gui::get() references — keeps WebServer
818+
// usable from tests that don't link the full gui library.
819+
void WebServer::stopAndJoinIoThreads()
820+
{
821+
if (ioc_) {
822+
ioc_->stop();
823+
}
824+
const auto self_id = std::this_thread::get_id();
825+
for (auto& t : threads_) {
826+
if (!t.joinable()) {
827+
continue;
828+
}
829+
if (t.get_id() == self_id) {
830+
// Self-join would raise EDEADLK. ioc_->stop() above unblocks the
831+
// worker so detaching is safe — the thread runs to completion on
832+
// its own.
833+
t.detach();
834+
} else {
835+
t.join();
836+
}
837+
}
838+
threads_.clear();
839+
}
840+
816841
WebServer::~WebServer()
817842
{
818843
// Wake any thread blocked in waitForStop() so it can return before
@@ -825,24 +850,12 @@ WebServer::~WebServer()
825850

826851
// The destructor fires during Tcl_Exit → atexit → ~OpenRoad chain.
827852
// By this point the Tcl interpreter is partially torn down and static
828-
// objects may be destroyed. Calling stop() (which joins 32 threads
829-
// and tears down boost::asio's reactor) triggers SIGSEGV because the
830-
// reactor's internal state references destroyed statics.
831-
//
832-
// Since the destructor only runs at process exit, the OS reclaims all
833-
// memory and closes all sockets. We just need to stop the threads so
834-
// the process can actually exit:
835-
if (ioc_) {
836-
ioc_->stop();
837-
}
838-
for (auto& t : threads_) {
839-
if (t.joinable()) {
840-
t.join();
841-
}
842-
}
843-
threads_.clear();
844-
// Close the Listener's acceptor and release the shared_ptr to it
845-
// before the io_context goes away.
853+
// objects may be destroyed. We avoid the full stop() path (which
854+
// tears down boost::asio's reactor and would crash on residual async
855+
// handlers referencing destroyed statics) — the OS reclaims memory
856+
// and sockets at process exit, so we only need to release the worker
857+
// threads so the process can actually exit.
858+
stopAndJoinIoThreads();
846859
if (shutdown_listener_) {
847860
shutdown_listener_();
848861
shutdown_listener_ = {};

0 commit comments

Comments
 (0)