Skip to content

feat(dbSta): store timing constraints in the .odb - #11260

Closed
oharboe wants to merge 1 commit into
The-OpenROAD-Project:masterfrom
oharboe:sdc-in-odb
Closed

feat(dbSta): store timing constraints in the .odb#11260
oharboe wants to merge 1 commit into
The-OpenROAD-Project:masterfrom
oharboe:sdc-in-odb

Conversation

@oharboe

@oharboe oharboe commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Draft / proposal. Single concern: make an .odb carry its own timing constraints.

Why

.odb already subsumes the netlist, the tech, placement, routing, the GCell grid, PDN, scan chains, power domains, global connect rules — and dont_touch (dbInst::setDoNotTouch). Timing constraints are the conspicuous omission.

So a flow carries a matching .sdc next to every .odb, and then has to work out which .sdc goes with which .odb. In ORFS that is a glob of results/, a dictionary sort, and "pick the greatest .sdc at or below this .odb's stem". It has picked wrong before. The only new freedom the split buys anyone is the freedom to load the wrong constraints.

This is probably the POLA .odb thinking all along — nobody got around to it, and we keep stubbing our toes on it. .sdc is not special. It is a human-authored input, consumed at one point in a flow, exactly like the LEF and the netlist that .odb already absorbs.

Small human-written .sdc files are a feature worth protecting. This change does not touch them, and does not touch write_sdc: constraints still export to text for inspection and for standalone OpenSTA bug reports. What changes is that a flow no longer has to route constraints between its own stages through a text format.

What this does

  • write_db stores the constraints in the block (a dbStringProperty named sta.sdc, the output of write_sdc -no_timestamp).
  • read_db -sdc replays them. Without the flag read_db behaves exactly as it does today — no behavior change for any existing script.
  • write_sdc is untouched.

Chesterton's Fence

Fence Status
odb must not depend on OpenSTA Intact. The payload is opaque to odb; the glue is in dbSta, on the existing dbStringProperty persistence. No new dependency edge.
Old binaries must still read new .odb Intact. A property needs no schema bump; an older OpenROAD ignores it.
Changing read_db semantics would surprise people Respected. Replay is opt-in.
write_sdc fidelity — a user's .sdc is Tcl and Sdc models less than the file says The risk is already taken downstream. ORFS canonicalizes constraints at synthesis today and every later stage reads write_sdc output. This adds no new exposure, and the tests below check the round trip byte for byte.
write_db must not break on designs with no constraints Capture is skipped silently when no liberty is linked; a failure warns and still writes the same .odb as before.

Correctness

write_dbread_db -sdcwrite_sdc reproduces the original constraints byte for byte on the largest public asap7 designs — swerv_wrapper (89,615 lines of constraints) and mock-cpu (94,253 lines).

Three regression tests in src/dbSta/test: constraints are stored by write_db and match write_sdc; read_db -sdc brings the design back constrained with no .sdc in sight; plain read_db still loads no constraints. Full //src/dbSta/... suite passes (2549 tests).

Performance

The second toe-stub, and the one that is paid continuously — at every stage of every run, and again every time somebody opens a large design in the GUI.

A human writes a handful of lines. write_sdc expands them into one explicit get_cells/get_ports per object, and the next stage re-litigates every one of those name lookups against the netlist.

design std cells human .sdc canonical .sdc object lookups
swerv_wrapper 141,318 454 B, 14 lines 3.00 MB, 89,615 lines 89,595
mock-cpu 47,168 5,024 B, 107 lines 5.10 MB, 94,253 lines 94,068
cva6 134,139 2,137 B, 37 lines 839 B, 15 lines 4

Loading results/.../3_place.odb plus its .sdc, median of 3, same machine, ASAP7 CI liberty set:

design read_db read_sdc before after (read_db -sdc) change
swerv_wrapper 239 ms 992 ms 1231 ms 1086 ms −12%
mock-cpu 71 ms 1075 ms 1146 ms 990 ms −14%
cva6 257 ms 1 ms 258 ms 258 ms

The 12–14% is real but it is not the point. The point is the read_sdc column: the constraints cost 4.2× (swerv_wrapper) and 15.1× (mock-cpu) what it costs to load the entire database. This PR stores the constraints as text and replays them through the same parser, so it only recovers the file and source overhead. The rest of that column is what a long-hand binary form — object references stored as odb ids, no Tcl, no name matching — is worth, and this PR is the place to put it.

cva6 is in the table as the honest control: its constraints do not expand, so there is nothing to win, and nothing is lost.

Reproducing on your own design

Nothing in the harness needs a public design. Given any .odb and its .sdc:

# baseline
read_liberty ...
set t0 [clock milliseconds]
read_db your.odb
set t1 [clock milliseconds]
read_sdc your.sdc
set t2 [clock milliseconds]
puts "read_db [expr {$t1-$t0}] ms, read_sdc [expr {$t2-$t1}] ms"

# store the constraints once
write_db your_with_sdc.odb
# with constraints in the .odb
read_liberty ...
set t0 [clock milliseconds]
read_db -sdc your_with_sdc.odb
puts "read_db -sdc [expr {[clock milliseconds]-$t0}] ms"

The numbers that matter are timings and line counts, so results can be reported without disclosing anything about the design.

Deliberately not in this PR

  • A long-hand binary form of the constraints. Where the performance actually is. Separate PR: it needs a schema bump and a much larger review, and the numbers above are the argument for it.
  • set_dont_use. Same dichotomy — a LibertyCell attribute that write_sdc never emits, so flows re-apply it from the environment at every stage, or forget to. Its own concern.
  • Default-on replay. After this has soaked.
  • Anything on the flow side. ORFS can adopt this whenever it likes; nothing here requires it to.
  • Platform physics — derating, RC setup, corner definitions. This makes an .odb constraint-complete, not run-complete.

Testing

bazelisk build //:openroad
bazelisk test //src/dbSta/... //src/odb/test/... //test/...

.odb already subsumes the netlist, tech, placement, routing and even
dont_touch. Timing constraints are the conspicuous omission, so a flow
has to carry a matching .sdc next to every .odb and then work out which
.sdc goes with which .odb. That inference is a guess, and the only new
freedom it buys anyone is the freedom to load the wrong constraints.

write_db now stores the constraints in the block, and read_db -sdc
replays them, so an .odb can describe itself.

The payload is a dbStringProperty, opaque to odb, so no odb -> sta
dependency is created and an older binary still reads the file. Replay
is opt-in, so read_db behaves exactly as before unless asked otherwise.
write_sdc is untouched and remains the way to export constraints for
inspection or for a standalone OpenSTA bug report.

Verified on the largest asap7 designs: write_db, then read_db -sdc,
then write_sdc reproduces the original constraints byte for byte
(swerv_wrapper, 89615 lines; mock-cpu, 94253 lines).

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enables storing and restoring timing constraints (SDC) directly within the OpenDB (.odb) database file, making it self-describing and removing the need for a separate .sdc file. Feedback on the changes suggests using the secure utl::ScopedTemporaryFile utility instead of a custom temporary file class to prevent insecure temporary file creation, and ensuring that SDC constraints are only restored if the database is successfully loaded without throwing an exception.

Comment thread src/dbSta/src/dbSta.cc
Comment on lines +28 to 30
#include <sstream>
#include <string>
#include <string_view>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Include the header for utl::ScopedTemporaryFile to securely handle temporary files.

Suggested change
#include <sstream>
#include <string>
#include <string_view>
#include <sstream>
#include <string>
#include <string_view>
#include "utl/ScopedTemporaryFile.h"

Comment thread src/dbSta/src/dbSta.cc
Comment on lines +467 to +541
namespace {

// sta::writeSdc only writes to a file. Round-trip through a temporary one
// until an ostream overload exists upstream in OpenSTA.
class TempSdcFile
{
public:
TempSdcFile()
{
path_ = std::filesystem::temp_directory_path()
/ ("openroad-sdc-" + std::to_string(::getpid()) + "-"
+ std::to_string(
reinterpret_cast<uintptr_t>(static_cast<void*>(this)))
+ ".sdc");
}
~TempSdcFile()
{
std::error_code ec;
std::filesystem::remove(path_, ec);
}
TempSdcFile(const TempSdcFile&) = delete;
TempSdcFile& operator=(const TempSdcFile&) = delete;

const std::filesystem::path& path() const { return path_; }

std::string read() const
{
std::ifstream in(path_, std::ios::binary);
if (!in) {
return {};
}
std::ostringstream buf;
buf << in.rdbuf();
return buf.str();
}

private:
std::filesystem::path path_;
};

} // namespace

void dbSta::saveSdcToDb()
{
odb::dbChip* chip = db_->getChip();
if (chip == nullptr) {
return;
}
odb::dbBlock* block = chip->getBlock();
if (block == nullptr) {
return;
}
// A pure-odb flow (no liberty, no linked network) has no constraints to
// save. Skip quietly: write_db must behave exactly as it did before for
// the flows that never had constraints in the first place.
if (!network_->isLinked() || network_->defaultLibertyLibrary() == nullptr) {
return;
}
const Sdc* sdc = cmdSdc();
if (sdc == nullptr) {
return;
}

std::string text;
try {
TempSdcFile temp;
writeSdc(sdc,
temp.path().string(),
/* leaf */ false,
/* native */ true,
/* digits */ 4,
/* gzip */ false,
/* no_timestamp */ true);
text = temp.read();
} catch (const std::exception& e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Creating temporary files with predictable names in shared directories (like /tmp) poses security risks (CWE-377: Insecure Temporary File) and race conditions. Instead of rolling a custom TempSdcFile class, use the existing, secure utl::ScopedTemporaryFile utility which handles secure file creation and automatic cleanup.

namespace {

// No custom TempSdcFile needed as we use utl::ScopedTemporaryFile.

}  // namespace

void dbSta::saveSdcToDb()
{
  odb::dbChip* chip = db_->getChip();
  if (chip == nullptr) {
    return;
  }
  odb::dbBlock* block = chip->getBlock();
  if (block == nullptr) {
    return;
  }
  // A pure-odb flow (no liberty, no linked network) has no constraints to
  // save. Skip quietly: write_db must behave exactly as it did before for
  // the flows that never had constraints in the first place.
  if (!network_->isLinked() || network_->defaultLibertyLibrary() == nullptr) {
    return;
  }
  const Sdc* sdc = cmdSdc();
  if (sdc == nullptr) {
    return;
  }

  std::string text;
  try {
    utl::ScopedTemporaryFile temp;
    writeSdc(sdc,
             temp.path(),
             /* leaf */ false,
             /* native */ true,
             /* digits */ 4,
             /* gzip */ false,
             /* no_timestamp */ true);
    std::ifstream in(temp.path(), std::ios::binary);
    if (in) {
      std::ostringstream buf;
      buf << in.rdbuf();
      text = buf.str();
    }
  } catch (const std::exception& e) {

Comment thread src/OpenRoad.cc
Comment on lines 560 to +568
try {
utl::InStreamHandler handler(filename, true);
readDb(handler.getStream());
} catch (const std::ios_base::failure& f) {
logger_->error(ORD, 54, "odb file {} is invalid: {}", filename, f.what());
}
if (restore_sdc) {
restoreSdcFromDb();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If reading the database stream fails and throws an exception, the database state might be invalid or incomplete. Replaying SDC constraints on an invalid database should be avoided. Only call restoreSdcFromDb() if the database was successfully loaded.

  bool success = true;
  try {
    utl::InStreamHandler handler(filename, true);
    readDb(handler.getStream());
  } catch (const std::ios_base::failure& f) {
    logger_->error(ORD, 54, "odb file {} is invalid: {}", filename, f.what());
    success = false;
  }
  if (success && restore_sdc) {
    restoreSdcFromDb();
  }

@oharboe

oharboe commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

out of sight, out of mind for now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant