Skip to content

Commit 78f8b03

Browse files
authored
Merge branch 'The-OpenROAD-Project:master' into grt_cugr_layer_idx
2 parents f699006 + 6dda608 commit 78f8b03

25 files changed

Lines changed: 591 additions & 155 deletions

.github/workflows/github-actions-on-label-create.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,86 @@ jobs:
8888
env:
8989
GITHUB_TOKEN: ${{ github.token }}
9090
UPSTREAM_PR: ${{ steps.send_pr.outputs.pr }}
91+
92+
- name: Request CODEOWNERS reviewers on upstream PR
93+
if: steps.send_pr.outputs.pr != ''
94+
env:
95+
GH_TOKEN: ${{ steps.resolve_token.outputs.token }}
96+
PR: ${{ steps.send_pr.outputs.pr }}
97+
UPSTREAM: ${{ env.UPSTREAM_OWNER }}/${{ env.UPSTREAM_REPO }}
98+
run: |
99+
set -euo pipefail
100+
# System Python on the runner is PEP 668 externally-managed; use a venv.
101+
python3 -m venv /tmp/codeowners-venv
102+
/tmp/codeowners-venv/bin/pip install --quiet pathspec
103+
/tmp/codeowners-venv/bin/python <<'PY'
104+
import base64, json, os, subprocess, sys
105+
from pathspec import GitIgnoreSpec
106+
107+
pr = os.environ["PR"]
108+
upstream = os.environ["UPSTREAM"]
109+
110+
pr_json = subprocess.check_output(
111+
["gh", "api", f"repos/{upstream}/pulls/{pr}"], text=True)
112+
pr_data = json.loads(pr_json)
113+
author = pr_data["user"]["login"]
114+
base_ref = pr_data["base"]["ref"]
115+
116+
# Authoritative CODEOWNERS is the one on the PR base branch.
117+
raw = subprocess.check_output(
118+
["gh", "api", "--method", "GET",
119+
f"repos/{upstream}/contents/.github/CODEOWNERS",
120+
"-f", f"ref={base_ref}", "--jq", ".content"], text=True).strip()
121+
codeowners = base64.b64decode(raw).decode()
122+
123+
rules = []
124+
for line in codeowners.splitlines():
125+
line = line.split("#", 1)[0].strip()
126+
if not line:
127+
continue
128+
pattern, *rule_owners = line.split()
129+
rules.append((GitIgnoreSpec.from_lines([pattern]), rule_owners))
130+
131+
# GitHub caps /pulls/{n}/files at 3000 even with --paginate; truly
132+
# enormous PRs will under-request owners for the overflow.
133+
files = subprocess.check_output(
134+
["gh", "api", f"repos/{upstream}/pulls/{pr}/files", "--paginate",
135+
"--jq", ".[].filename"], text=True).splitlines()
136+
137+
owners = set()
138+
for path in files:
139+
matched = None
140+
for spec, rule_owners in rules:
141+
if spec.match_file(path):
142+
matched = rule_owners # last match wins
143+
if matched:
144+
owners.update(o.lstrip("@") for o in matched)
145+
146+
# CODEOWNERS lists teams as "org/slug"; the REST endpoint wants the
147+
# bare slug in team_reviewers.
148+
team_slugs = sorted(t.split("/", 1)[1] for t in owners if "/" in t)
149+
users = sorted(o for o in owners
150+
if "/" not in o and o.lower() != author.lower())
151+
152+
if not (team_slugs or users):
153+
print("No CODEOWNERS-matched reviewers.")
154+
sys.exit(0)
155+
156+
# Use the REST POST endpoint directly: `gh pr edit --add-reviewer`
157+
# runs a GraphQL query that needs read:org, which our tokens don't
158+
# have. POST /pulls/{n}/requested_reviewers only writes, so the
159+
# existing repo / pull-requests:write scope is enough. Each array
160+
# is capped at 15 per call.
161+
def request(body):
162+
print("Requesting:", body)
163+
subprocess.run(
164+
["gh", "api", "--method", "POST",
165+
f"repos/{upstream}/pulls/{pr}/requested_reviewers",
166+
"--input", "-"],
167+
input=json.dumps(body), text=True, check=True)
168+
169+
for i in range(0, len(team_slugs), 15):
170+
request({"team_reviewers": team_slugs[i:i + 15]})
171+
for i in range(0, len(users), 15):
172+
request({"reviewers": users[i:i + 15]})
173+
PY

src/dbSta/src/dbSdcNetwork.cc

Lines changed: 130 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <memory>
99
#include <string>
1010
#include <string_view>
11+
#include <utility>
1112

1213
#include "spdlog/fmt/fmt.h"
1314
#include "sta/NetworkClass.hh"
@@ -66,52 +67,23 @@ InstanceSeq dbSdcNetwork::findInstancesMatching(
6667
void dbSdcNetwork::findInstancesMatching1(const PatternMatch* pattern,
6768
InstanceSeq& insts) const
6869
{
69-
// If the pattern is a literal (no wildcards and not a regex), return
70-
// immediately as the literal lookup has already failed in the caller.
70+
// Literal pattern: serve from the precomputed pathological-path map.
71+
// Most designs contribute zero entries to this map, so the lookup is
72+
// O(1) and the miss path simply falls through.
7173
if (!pattern->isRegexp() && !pattern->hasWildcards()) {
74+
const SdcPathToInstMap& map = sdcPathToInstMap();
75+
auto it = map.find(pattern->pattern());
76+
if (it != map.end()) {
77+
insts.push_back(it->second);
78+
}
7279
return;
7380
}
74-
// A recursive lambda to traverse the design hierarchy with a depth-first
75-
// search (DFS).
76-
// It builds the hierarchical path incrementally using fmt::memory_buffer to
77-
// avoid expensive std::string allocations and copies at each step.
78-
std::function<void(Instance*, fmt::memory_buffer&)> dfs_search
79-
= [&, this](Instance* instance, fmt::memory_buffer& path_buffer) -> void {
80-
// Iterate over the children of the current instance.
81-
std::unique_ptr<InstanceChildIterator> child_iter{childIterator(instance)};
82-
while (child_iter->hasNext()) {
83-
Instance* child = child_iter->next();
84-
85-
// Save the current size of the buffer to restore it later.
86-
const size_t original_size = path_buffer.size();
87-
88-
// Build the child's full path name incrementally.
89-
if (original_size > 0) {
90-
path_buffer.push_back(pathDivider());
91-
}
92-
path_buffer.append(std::string_view(name(child)));
93-
94-
// Check if the child instance name matches the pattern.
95-
// Add a null terminator for C-style string compatibility.
96-
path_buffer.push_back('\0');
97-
if (pattern->match(staToSdc(path_buffer.data()))) {
98-
insts.push_back(child);
99-
}
100-
path_buffer.resize(path_buffer.size() - 1); // Remove the null terminator
101-
102-
// Recurse into the child's hierarchy if it's not a leaf.
103-
if (!isLeaf(child)) {
104-
dfs_search(child, path_buffer);
105-
}
106-
107-
// Restore the buffer to its original state for the next sibling.
108-
path_buffer.resize(original_size);
109-
}
110-
};
111-
112-
// Start the search from the top-level instance.
113-
fmt::memory_buffer path_buffer;
114-
dfs_search(topInstance(), path_buffer);
81+
visitAllInstancesSdcPath(
82+
[&](Instance* child, const std::string& sdc_path, bool /*any_div*/) {
83+
if (pattern->match(sdc_path)) {
84+
insts.push_back(child);
85+
}
86+
});
11587
}
11688

11789
NetSeq dbSdcNetwork::findNetsMatching(const Instance*,
@@ -243,4 +215,119 @@ Pin* dbSdcNetwork::findPin(std::string_view path_name) const
243215
return pin;
244216
}
245217

218+
void dbSdcNetwork::visitAllInstancesSdcPath(const SdcPathVisitor& visitor) const
219+
{
220+
// Build paths incrementally in a fmt::memory_buffer to avoid the
221+
// per-step std::string allocations a naive DFS would incur. Using a
222+
// generic recursive lambda (auto& self) instead of a std::function
223+
// skips type erasure and the heap allocation it can incur.
224+
// The any_div flag propagates down so the visitor can cheaply tell
225+
// whether sdc_path's recursive splitter resolution would fail.
226+
auto rec = [&](auto& self,
227+
Instance* parent,
228+
fmt::memory_buffer& buf,
229+
bool any_div) -> void {
230+
std::unique_ptr<InstanceChildIterator> it{childIterator(parent)};
231+
while (it->hasNext()) {
232+
Instance* child = it->next();
233+
const size_t orig_size = buf.size();
234+
if (orig_size > 0) {
235+
buf.push_back(pathDivider());
236+
}
237+
const std::string leaf = name(child);
238+
// Short-circuit: once a subtree is pathological, descendants
239+
// inherit that flag without re-scanning the leaf string.
240+
const bool subtree_any_div
241+
= any_div || leaf.find(pathDivider()) != std::string::npos;
242+
buf.append(std::string_view(leaf));
243+
buf.push_back('\0'); // null-terminate for staToSdc's C-string input
244+
visitor(child, staToSdc(buf.data()), subtree_any_div);
245+
buf.resize(buf.size() - 1);
246+
if (!isLeaf(child)) {
247+
self(self, child, buf, subtree_any_div);
248+
}
249+
buf.resize(orig_size);
250+
}
251+
};
252+
fmt::memory_buffer buf;
253+
rec(rec, topInstance(), buf, false);
254+
}
255+
256+
const dbSdcNetwork::SdcPathToInstMap& dbSdcNetwork::sdcPathToInstMap() const
257+
{
258+
if (!cache_built_) {
259+
visitAllInstancesSdcPath(
260+
[&](Instance* inst, std::string sdc_path, bool any_div) {
261+
// Only pathological entries (those the recursive splitter in
262+
// findInstance cannot resolve) need to live in the cache.
263+
if (any_div) {
264+
inst_to_sdc_path_.emplace(inst, sdc_path);
265+
sdc_path_to_inst_.emplace(std::move(sdc_path), inst);
266+
}
267+
});
268+
cache_built_ = true;
269+
}
270+
return sdc_path_to_inst_;
271+
}
272+
273+
bool dbSdcNetwork::hasPathologicalPath(const Instance* inst) const
274+
{
275+
const Instance* top = topInstance();
276+
for (const Instance* a = inst; a && a != top; a = network_->parent(a)) {
277+
if (name(a).find(pathDivider()) != std::string::npos) {
278+
return true;
279+
}
280+
}
281+
return false;
282+
}
283+
284+
void dbSdcNetwork::insertEntry(Instance* inst) const
285+
{
286+
std::string sdc_path = SdcNetwork::pathName(inst);
287+
inst_to_sdc_path_.emplace(inst, sdc_path);
288+
sdc_path_to_inst_.emplace(std::move(sdc_path), inst);
289+
}
290+
291+
void dbSdcNetwork::eraseEntry(const Instance* inst) const
292+
{
293+
auto rev_it = inst_to_sdc_path_.find(inst);
294+
if (rev_it == inst_to_sdc_path_.end()) {
295+
return;
296+
}
297+
sdc_path_to_inst_.erase(rev_it->second);
298+
inst_to_sdc_path_.erase(rev_it);
299+
}
300+
301+
void dbSdcNetwork::onInstCreated(Instance* inst)
302+
{
303+
if (!cache_built_) {
304+
return;
305+
}
306+
if (!hasPathologicalPath(inst)) {
307+
return;
308+
}
309+
insertEntry(inst);
310+
}
311+
312+
void dbSdcNetwork::onInstDestroyed(Instance* inst)
313+
{
314+
if (!cache_built_) {
315+
return;
316+
}
317+
eraseEntry(inst);
318+
}
319+
320+
void dbSdcNetwork::onInstRenamed(Instance* inst)
321+
{
322+
if (!cache_built_) {
323+
return;
324+
}
325+
// Erase by Instance* (uses the reverse map) — no need to reconstruct
326+
// the pre-rename path. Re-insert if still pathological.
327+
eraseEntry(inst);
328+
if (hasPathologicalPath(inst)) {
329+
insertEntry(inst);
330+
}
331+
}
332+
246333
} // namespace sta

src/dbSta/src/dbSdcNetwork.hh

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
// Copyright (c) 2019-2025, The OpenROAD Authors
33

44
#pragma once
5+
#include <cstddef>
6+
#include <functional>
7+
#include <string>
58
#include <string_view>
9+
#include <unordered_map>
610

711
#include "sta/Network.hh"
812
#include "sta/NetworkClass.hh"
@@ -22,6 +26,25 @@ class dbSdcNetwork : public SdcNetwork
2226
PinSeq findPinsMatching(const Instance* instance,
2327
const PatternMatch* pattern) const override;
2428

29+
// Drop the cache. Used on subtree-wide edits (parent change, modInst
30+
// destroy) where surgical fix-up would be more error-prone than letting
31+
// the next literal lookup rebuild lazily.
32+
void invalidateSdcPathToInstMap()
33+
{
34+
sdc_path_to_inst_.clear();
35+
inst_to_sdc_path_.clear();
36+
cache_built_ = false;
37+
}
38+
39+
// Incremental cache maintenance for hierarchy edits. Each is a no-op
40+
// when the cache hasn't been built yet (lazy property preserved) or
41+
// when the affected instance has no path component containing the
42+
// divider — the cache only ever holds the small set of "pathological"
43+
// entries that findInstance's recursive splitter cannot resolve.
44+
void onInstCreated(Instance* inst);
45+
void onInstDestroyed(Instance* inst);
46+
void onInstRenamed(Instance* inst);
47+
2548
protected:
2649
void findInstancesMatching1(const PatternMatch* pattern,
2750
InstanceSeq& insts) const;
@@ -31,6 +54,50 @@ class dbSdcNetwork : public SdcNetwork
3154
PinSeq& pins) const;
3255
Pin* findPin(std::string_view path_name) const override;
3356
using SdcNetwork::findPin;
57+
58+
private:
59+
// Heterogeneous string hashing so callers can look up by std::string_view
60+
// without allocating a temporary std::string for each query.
61+
struct TransparentStringHash
62+
{
63+
using is_transparent = void;
64+
size_t operator()(std::string_view s) const noexcept
65+
{
66+
return std::hash<std::string_view>{}(s);
67+
}
68+
};
69+
using SdcPathToInstMap = std::unordered_map<std::string,
70+
Instance*,
71+
TransparentStringHash,
72+
std::equal_to<>>;
73+
// Reverse map for O(1) erase-by-instance during destroy/rename.
74+
using InstToSdcPathMap = std::unordered_map<const Instance*, std::string>;
75+
using SdcPathVisitor
76+
= std::function<void(Instance*, std::string sdc_path, bool any_div)>;
77+
78+
// DFS the hierarchy, invoking visitor(child, sdc_path, any_div) once per
79+
// instance. any_div is true when sdc_path or any ancestor's leaf name
80+
// contains the divider — i.e. findInstance cannot resolve sdc_path.
81+
void visitAllInstancesSdcPath(const SdcPathVisitor& visitor) const;
82+
83+
// True iff inst itself or any ancestor has a STA-form leaf name
84+
// containing the path divider (an escaped Verilog identifier such as
85+
// "\foo/bar"). These are the only entries we cache.
86+
bool hasPathologicalPath(const Instance* inst) const;
87+
88+
// Insert / erase keep both maps consistent. Callers must have ensured
89+
// the cache is built; insertEntry additionally requires inst pathological.
90+
void insertEntry(Instance* inst) const;
91+
void eraseEntry(const Instance* inst) const;
92+
93+
// Lazy full-path -> Instance lookup, used by findInstancesMatching1 to
94+
// resolve literal SDC patterns that the hierarchy walker missed (e.g. a
95+
// Verilog escaped identifier "\foo/bar"). Only pathological entries are
96+
// stored, so memory is O(escaped-identifier-count), not O(N).
97+
const SdcPathToInstMap& sdcPathToInstMap() const;
98+
mutable SdcPathToInstMap sdc_path_to_inst_;
99+
mutable InstToSdcPathMap inst_to_sdc_path_;
100+
mutable bool cache_built_ = false;
34101
};
35102

36103
} // namespace sta

0 commit comments

Comments
 (0)