Add MANIFOLD_TIMING build option + perfPhases benchmark (#1670)

* Add MANIFOLD_TIMING build option for release-build phase timing

MANIFOLD_DEBUG enables the Timer instrumentation in Boolean3 but also
the DEBUG_ASSERTs in hot kernels (Interpolate, Intersect, Kernel11),
inflating measurements. MANIFOLD_TIMING gates just the Timer code.

Also splits the Intersections ctor segment into four sub-phases
(Intersect12 P->Q, Intersect12 Q->P, Winding03 P, Winding03 Q) so the
verbose output maps 1:1 to the four ctor cancel checkpoints in #1669.
MANIFOLD_DEBUG is a strict superset, so existing builds get these
additional sub-phase lines alongside the Intersections total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add perfPhases benchmark

Reusable harness for MANIFOLD_TIMING=ON (or MANIFOLD_DEBUG=ON). Runs
Add/Subtract/Intersect on sphere (8 scales) and MengerSponge(3,4),
forcing sponge construction before timing the ops. Gated on
MANIFOLD_DEBUG OR MANIFOLD_TIMING (matches minimizeTestcase pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zach Loafman
2026-04-22 17:18:33 +12:00
committed by GitHub
co-authored by Claude Opus 4.7
parent adaf64c4a7
commit 8d28267ca1
7 changed files with 123 additions and 9 deletions
+1
View File
@@ -40,6 +40,7 @@ set(
option(MANIFOLD_CROSS_SECTION "Build CrossSection for 2D support" ON)
option(MANIFOLD_DEBUG "Enable debug tracing/timing" OFF)
option(MANIFOLD_ASSERT "Enable assertions - requires MANIFOLD_DEBUG" OFF)
option(MANIFOLD_TIMING "Enable Boolean3 phase timing without debug overhead" OFF)
option(MANIFOLD_STRICT "Treat compile warnings as fatal build errors" OFF)
option(
MANIFOLD_DOWNLOADS
+10
View File
@@ -21,6 +21,16 @@ target_link_libraries(perfTest manifold)
target_compile_options(perfTest PRIVATE ${MANIFOLD_FLAGS})
exportbin(perfTest)
if(MANIFOLD_DEBUG OR MANIFOLD_TIMING)
add_executable(perfPhases perf_phases.cpp)
target_link_libraries(perfPhases manifold samples)
target_include_directories(
perfPhases PRIVATE ${PROJECT_SOURCE_DIR}/samples/include
)
target_compile_options(perfPhases PRIVATE ${MANIFOLD_FLAGS})
exportbin(perfPhases)
endif()
if(MANIFOLD_PAR AND NOT MSVC)
add_executable(stlTest stl_test.cpp)
target_link_libraries(stlTest PRIVATE manifold TBB::tbb)
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2026 The Manifold Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Prints per-phase Boolean3 wall-clock timings on representative workloads.
// Requires MANIFOLD_TIMING=ON (or MANIFOLD_DEBUG=ON) so Timer::Print fires.
// Used for cancel-latency analysis and for identifying dominant phases.
#include <chrono>
#include <iostream>
#include <string>
#include "manifold/manifold.h"
#include "samples.h"
using namespace manifold;
namespace {
// Runs one boolean op of each kind (Add, Subtract, Intersect) against the
// same input pair. Each is a single boolean so Timer output is clean.
void BenchAll(const std::string& workload, const Manifold& a,
const Manifold& b) {
struct Op {
const char* name;
Manifold (*apply)(const Manifold&, const Manifold&);
};
const Op ops[] = {
{"add", [](const Manifold& x, const Manifold& y) { return x + y; }},
{"subtract", [](const Manifold& x, const Manifold& y) { return x - y; }},
{"intersect", [](const Manifold& x, const Manifold& y) { return x ^ y; }},
};
for (const auto& op : ops) {
std::cout << "=== " << workload << ": " << op.name
<< ", nTri(a) = " << a.NumTri() << " ===" << std::endl;
auto t0 = std::chrono::high_resolution_clock::now();
Manifold result = op.apply(a, b);
result.NumTri();
auto t1 = std::chrono::high_resolution_clock::now();
std::cout << "total = " << std::chrono::duration<double>(t1 - t0).count()
<< " sec\n"
<< std::endl;
}
}
} // namespace
int main(int argc, char** argv) {
ManifoldParams().verbose = 2;
// Sphere at escalating scales — uniform geometry, single boolean per op
// so Timer output is readable.
for (int i = 0; i < 8; ++i) {
const int segments = (8 << i) * 4;
Manifold sphere = Manifold::Sphere(1, segments);
Manifold sphere2 = sphere.Translate(vec3(0.5));
BenchAll("Sphere(" + std::to_string(segments) + ")", sphere, sphere2);
}
// Triangulator-heavy: Menger sponge. Forcing sponge.NumTri() first
// separates the sponge's internal CSG construction from the measured
// ops, so `total` reflects just each boolean.
for (int level : {3, 4}) {
Manifold sponge = MengerSponge(level);
Manifold sponge2 = sponge.Translate(vec3(0.3));
sponge.NumTri(); // force construction before timing the ops
BenchAll("MengerSponge(" + std::to_string(level) + ")", sponge, sponge2);
}
}
+5 -1
View File
@@ -18,8 +18,9 @@
#include <memory>
#include <vector>
#ifdef MANIFOLD_DEBUG
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
#include <chrono>
#include <iostream>
#endif
#include "./math.h"
@@ -666,6 +667,9 @@ void Diff(const std::vector<T>& a, const std::vector<T>& b) {
std::cout << std::endl;
}
#endif
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
struct Timer {
std::chrono::high_resolution_clock::time_point start, end;
+1
View File
@@ -78,6 +78,7 @@ set(
OPTIONS
MANIFOLD_DEBUG
MANIFOLD_ASSERT
MANIFOLD_TIMING
MANIFOLD_CROSS_SECTION
TRACY_ENABLE
TRACY_MEMORY_USAGE
+24 -3
View File
@@ -498,9 +498,11 @@ Boolean3::Boolean3(const Manifold::Impl& inP, const Manifold::Impl& inQ,
return;
}
#ifdef MANIFOLD_DEBUG
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
Timer intersections;
intersections.Start();
Timer intersect12P, intersect12Q, winding03P, winding03Q;
intersect12P.Start();
#endif
// Level 3
@@ -508,7 +510,14 @@ Boolean3::Boolean3(const Manifold::Impl& inP, const Manifold::Impl& inQ,
// that intersect, and record the direction the edge is passing through the
// triangle.
xv12_ = Intersect12<true>(inP, inQ, expandP_);
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
intersect12P.Stop();
intersect12Q.Start();
#endif
xv21_ = Intersect12<false>(inP, inQ, expandP_);
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
intersect12Q.Stop();
#endif
if (xv12_.x12.size() > INT_MAX_SZ || xv21_.x12.size() > INT_MAX_SZ) {
valid = false;
@@ -517,14 +526,26 @@ Boolean3::Boolean3(const Manifold::Impl& inP, const Manifold::Impl& inQ,
// Compute winding numbers of all vertices using flood fill
// Vertices on the same connected component have the same winding number
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
winding03P.Start();
#endif
w03_ = Winding03<true>(inP, inQ, xv12_.p1q2, expandP_);
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
winding03P.Stop();
winding03Q.Start();
#endif
w30_ = Winding03<false>(inP, inQ, xv21_.p1q2, expandP_);
#ifdef MANIFOLD_DEBUG
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
winding03Q.Stop();
intersections.Stop();
if (ManifoldParams().verbose >= 2) {
intersections.Print("Intersections");
intersect12P.Print(" Intersect12 P->Q");
intersect12Q.Print(" Intersect12 Q->P");
winding03P.Print(" Winding03 P");
winding03Q.Print(" Winding03 Q");
intersections.Print("Intersections (total)");
}
#endif
}
+5 -5
View File
@@ -651,7 +651,7 @@ namespace manifold {
Manifold::Impl Boolean3::Result(OpType op) const {
ZoneScoped;
#ifdef MANIFOLD_DEBUG
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
Timer assemble;
assemble.Start();
#endif
@@ -825,7 +825,7 @@ Manifold::Impl Boolean3::Result(OpType op) const {
vP2R.clear();
vQ2R.clear();
#ifdef MANIFOLD_DEBUG
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
assemble.Stop();
Timer triangulate;
triangulate.Start();
@@ -838,7 +838,7 @@ Manifold::Impl Boolean3::Result(OpType op) const {
outR.ReorderHalfedges();
#ifdef MANIFOLD_DEBUG
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
triangulate.Stop();
Timer simplify;
simplify.Start();
@@ -859,7 +859,7 @@ Manifold::Impl Boolean3::Result(OpType op) const {
DEBUG_ASSERT(outR.Is2Manifold(), logicErr,
"simplified mesh is not 2-manifold!");
#ifdef MANIFOLD_DEBUG
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
simplify.Stop();
Timer sort;
sort.Start();
@@ -869,7 +869,7 @@ Manifold::Impl Boolean3::Result(OpType op) const {
outR.SortGeometry();
outR.IncrementMeshIDs();
#ifdef MANIFOLD_DEBUG
#if defined(MANIFOLD_DEBUG) || defined(MANIFOLD_TIMING)
sort.Stop();
if (ManifoldParams().verbose >= 2) {
assemble.Print("Assembly");