A http2 setting to ensure safe rollout of tcp receive buffer auto-sizing and peer-state based framing experiments (#31404)

* A http2 setting to ensure safe rollout of tcp receive buffer auto-sizing and peer-state based framing experiments

* fix comments + sanity + iwyu

* comments

* update per comments

* comments

* iwyu

* address comments

* remove if check
pull/31621/head
Vignesh Babu 2 years ago committed by GitHub
parent 26bc68c4af
commit 73ea66d8ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      include/grpc/impl/codegen/grpc_types.h
  2. 46
      src/core/ext/transport/chttp2/transport/chttp2_transport.cc
  3. 16
      src/core/ext/transport/chttp2/transport/flow_control.cc
  4. 27
      src/core/ext/transport/chttp2/transport/flow_control.h
  5. 4
      src/core/ext/transport/chttp2/transport/http2_settings.cc
  6. 14
      src/core/ext/transport/chttp2/transport/http2_settings.h
  7. 4
      src/core/ext/transport/chttp2/transport/internal.h
  8. 2
      src/core/lib/event_engine/posix_engine/posix_endpoint.cc
  9. 5
      src/core/lib/iomgr/tcp_posix.cc
  10. 98
      test/core/transport/chttp2/flow_control_test.cc
  11. 11
      tools/codegen/core/gen_settings_ids.py

@ -235,6 +235,13 @@ typedef struct {
/** Should we allow receipt of true-binary data on http2 connections?
Defaults to on (1) */
#define GRPC_ARG_HTTP2_ENABLE_TRUE_BINARY "grpc.http2.true_binary"
/** An experimental channel arg which determines whether the perferred crypto
* frame size http2 setting sent to the peer at startup. If set to 0 (false
* - default), the preferred frame size is not sent to the peer. Otherwise it
* sends a default preferred crypto frame size value of 4GB to the peer at
* the startup of each connection. */
#define GRPC_ARG_EXPERIMENTAL_HTTP2_PREFERRED_CRYPTO_FRAME_SIZE \
"grpc.experimental.http2.enable_preferred_frame_size"
/** After a duration of this time the client/server pings its peer to see if the
transport is still alive. Int valued, milliseconds. */
#define GRPC_ARG_KEEPALIVE_TIME_MS "grpc.keepalive_time_ms"

@ -61,7 +61,6 @@
#include "src/core/lib/channel/channel_args.h"
#include "src/core/lib/debug/stats.h"
#include "src/core/lib/debug/stats_data.h"
#include "src/core/lib/experiments/experiments.h"
#include "src/core/lib/gpr/useful.h"
#include "src/core/lib/gprpp/bitset.h"
#include "src/core/lib/gprpp/debug_location.h"
@ -323,6 +322,13 @@ static void read_channel_args(grpc_chttp2_transport* t,
t->keepalive_permit_without_calls =
channel_args.GetBool(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS)
.value_or(false);
// Only send the prefered rx frame size http2 setting if we are instructed
// to auto size the buffers allocated at tcp level and we also can adjust
// sending frame size.
t->enable_preferred_rx_crypto_frame_advertisement =
channel_args
.GetBool(GRPC_ARG_EXPERIMENTAL_HTTP2_PREFERRED_CRYPTO_FRAME_SIZE)
.value_or(false);
if (channel_args.GetBool(GRPC_ARG_ENABLE_CHANNELZ)
.value_or(GRPC_ENABLE_CHANNELZ_DEFAULT)) {
@ -393,6 +399,16 @@ static void read_channel_args(grpc_chttp2_transport* t,
is_client ? "clients" : "servers");
}
}
if (t->enable_preferred_rx_crypto_frame_advertisement) {
const grpc_chttp2_setting_parameters* sp =
&grpc_chttp2_settings_parameters
[GRPC_CHTTP2_SETTINGS_GRPC_PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE];
queue_setting_update(
t, GRPC_CHTTP2_SETTINGS_GRPC_PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE,
grpc_core::Clamp(INT_MAX, static_cast<int>(sp->min_value),
static_cast<int>(sp->max_value)));
}
}
static void init_transport_keepalive_settings(grpc_chttp2_transport* t) {
@ -864,16 +880,17 @@ static void write_action(void* gt, grpc_error_handle /*error*/) {
grpc_chttp2_transport* t = static_cast<grpc_chttp2_transport*>(gt);
void* cl = t->cl;
t->cl = nullptr;
// If the peer_state_based_framing experiment is set to true,
// choose max_frame_size as 2 * max http2 frame size of peer. If peer is under
// high memory pressure, then it would advertise a smaller max http2 frame
// size. With this logic, the sender would automatically reduce the sending
// frame size as well.
// Choose max_frame_size as the prefered rx crypto frame size indicated by the
// peer.
int max_frame_size =
grpc_core::IsPeerStateBasedFramingEnabled()
? 2 * t->settings[GRPC_PEER_SETTINGS]
[GRPC_CHTTP2_SETTINGS_MAX_FRAME_SIZE]
: INT_MAX;
t->settings
[GRPC_PEER_SETTINGS]
[GRPC_CHTTP2_SETTINGS_GRPC_PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE];
// Note: max frame size is 0 if the remote peer does not support adjusting the
// sending frame size.
if (max_frame_size == 0) {
max_frame_size = INT_MAX;
}
grpc_endpoint_write(
t->ep, &t->outbuf,
GRPC_CLOSURE_INIT(&t->write_action_end_locked, write_action_end, t,
@ -2301,6 +2318,15 @@ void grpc_chttp2_act_on_flowctl_action(
queue_setting_update(t, GRPC_CHTTP2_SETTINGS_MAX_FRAME_SIZE,
action.max_frame_size());
});
if (t->enable_preferred_rx_crypto_frame_advertisement) {
WithUrgency(
t, action.preferred_rx_crypto_frame_size_update(),
GRPC_CHTTP2_INITIATE_WRITE_SEND_SETTINGS, [t, &action]() {
queue_setting_update(
t, GRPC_CHTTP2_SETTINGS_GRPC_PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE,
action.preferred_rx_crypto_frame_size());
});
}
}
static grpc_error_handle try_http_parsing(grpc_chttp2_transport* t) {

@ -366,6 +366,22 @@ FlowControlAction TransportFlowControl::PeriodicUpdate() {
16384, 16777215)),
&action, &FlowControlAction::set_send_max_frame_size_update);
}
if (IsTcpFrameSizeTuningEnabled()) {
// Advertise PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE to peer. By advertising
// PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE to the peer, we are informing the
// peer that we have tcp frame size tuning enabled and we inform it of our
// prefered rx frame sizes. The prefered rx frame size is determined as:
// Clamp(target_frame_size_ * 2, 16384, 0x7fffffff). In the future, this
// maybe updated to a different function of the memory pressure.
UpdateSetting(
GRPC_CHTTP2_SETTINGS_GRPC_PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE,
&target_preferred_rx_crypto_frame_size_,
Clamp(static_cast<unsigned int>(target_frame_size_ * 2), 16384u,
0x7ffffffu),
&action,
&FlowControlAction::set_preferred_rx_crypto_frame_size_update);
}
}
return UpdateAction(action);
}

@ -21,6 +21,7 @@
#include <grpc/support/port_platform.h>
#include <limits.h>
#include <stdint.h>
#include <iosfwd>
@ -59,6 +60,7 @@ static constexpr uint32_t kMinPositiveInitialWindowSize = 1024;
static constexpr const uint32_t kMaxInitialWindowSize = (1u << 30);
// The maximum per-stream flow control window delta to advertise.
static constexpr const int64_t kMaxWindowDelta = (1u << 20);
static constexpr const int kDefaultPreferredRxCryptoFrameSize = INT_MAX;
// TODO(ctiller): clean up when flow_control_fixes is enabled by default
static constexpr uint32_t kFrameSize = 1024 * 1024;
@ -92,8 +94,14 @@ class FlowControlAction {
Urgency send_max_frame_size_update() const {
return send_max_frame_size_update_;
}
Urgency preferred_rx_crypto_frame_size_update() const {
return preferred_rx_crypto_frame_size_update_;
}
uint32_t initial_window_size() const { return initial_window_size_; }
uint32_t max_frame_size() const { return max_frame_size_; }
uint32_t preferred_rx_crypto_frame_size() const {
return preferred_rx_crypto_frame_size_;
}
FlowControlAction& set_send_stream_update(Urgency u) {
send_stream_update_ = u;
@ -115,6 +123,12 @@ class FlowControlAction {
max_frame_size_ = update;
return *this;
}
FlowControlAction& set_preferred_rx_crypto_frame_size_update(
Urgency u, uint32_t update) {
preferred_rx_crypto_frame_size_update_ = u;
preferred_rx_crypto_frame_size_ = update;
return *this;
}
static const char* UrgencyString(Urgency u);
std::string DebugString() const;
@ -129,7 +143,11 @@ class FlowControlAction {
(send_initial_window_update_ == Urgency::NO_ACTION_NEEDED ||
initial_window_size_ == other.initial_window_size_) &&
(send_max_frame_size_update_ == Urgency::NO_ACTION_NEEDED ||
max_frame_size_ == other.max_frame_size_);
max_frame_size_ == other.max_frame_size_) &&
(preferred_rx_crypto_frame_size_update_ ==
Urgency::NO_ACTION_NEEDED ||
preferred_rx_crypto_frame_size_ ==
other.preferred_rx_crypto_frame_size_);
}
private:
@ -137,8 +155,10 @@ class FlowControlAction {
Urgency send_transport_update_ = Urgency::NO_ACTION_NEEDED;
Urgency send_initial_window_update_ = Urgency::NO_ACTION_NEEDED;
Urgency send_max_frame_size_update_ = Urgency::NO_ACTION_NEEDED;
Urgency preferred_rx_crypto_frame_size_update_ = Urgency::NO_ACTION_NEEDED;
uint32_t initial_window_size_ = 0;
uint32_t max_frame_size_ = 0;
uint32_t preferred_rx_crypto_frame_size_ = 0;
};
std::ostream& operator<<(std::ostream& out, FlowControlAction::Urgency urgency);
@ -232,6 +252,9 @@ class TransportFlowControl final {
int64_t target_window() const;
int64_t target_frame_size() const { return target_frame_size_; }
int64_t target_preferred_rx_crypto_frame_size() const {
return target_preferred_rx_crypto_frame_size_;
}
BdpEstimator* bdp_estimator() { return &bdp_estimator_; }
@ -291,6 +314,8 @@ class TransportFlowControl final {
int64_t remote_window_ = kDefaultWindow;
int64_t target_initial_window_size_ = kDefaultWindow;
int64_t target_frame_size_ = kDefaultFrameSize;
int64_t target_preferred_rx_crypto_frame_size_ =
kDefaultPreferredRxCryptoFrameSize;
int64_t announced_window_ = kDefaultWindow;
uint32_t acked_init_window_ = kDefaultWindow;
};

@ -25,7 +25,7 @@
#include "src/core/lib/gpr/useful.h"
#include "src/core/lib/transport/http2_errors.h"
const uint16_t grpc_setting_id_to_wire_id[] = {1, 2, 3, 4, 5, 6, 65027};
const uint16_t grpc_setting_id_to_wire_id[] = {1, 2, 3, 4, 5, 6, 65027, 65028};
bool grpc_wire_id_to_setting_id(uint32_t wire_id, grpc_chttp2_setting_id* out) {
uint32_t i = wire_id - 1;
@ -59,4 +59,6 @@ const grpc_chttp2_setting_parameters
GRPC_CHTTP2_CLAMP_INVALID_VALUE, GRPC_HTTP2_PROTOCOL_ERROR},
{"GRPC_ALLOW_TRUE_BINARY_METADATA", 0u, 0u, 1u,
GRPC_CHTTP2_CLAMP_INVALID_VALUE, GRPC_HTTP2_PROTOCOL_ERROR},
{"GRPC_PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE", 0u, 16384u, 2147483647u,
GRPC_CHTTP2_CLAMP_INVALID_VALUE, GRPC_HTTP2_PROTOCOL_ERROR},
};

@ -25,7 +25,7 @@
#include <stdint.h>
enum grpc_chttp2_setting_id {
typedef enum {
GRPC_CHTTP2_SETTINGS_HEADER_TABLE_SIZE = 0, /* wire id 1 */
GRPC_CHTTP2_SETTINGS_ENABLE_PUSH = 1, /* wire id 2 */
GRPC_CHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS = 2, /* wire id 3 */
@ -33,10 +33,11 @@ enum grpc_chttp2_setting_id {
GRPC_CHTTP2_SETTINGS_MAX_FRAME_SIZE = 4, /* wire id 5 */
GRPC_CHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE = 5, /* wire id 6 */
GRPC_CHTTP2_SETTINGS_GRPC_ALLOW_TRUE_BINARY_METADATA = 6, /* wire id 65027 */
};
#define GRPC_CHTTP2_NUM_SETTINGS 7
GRPC_CHTTP2_SETTINGS_GRPC_PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE =
7, /* wire id 65028 */
} grpc_chttp2_setting_id;
#define GRPC_CHTTP2_NUM_SETTINGS 8
extern const uint16_t grpc_setting_id_to_wire_id[];
bool grpc_wire_id_to_setting_id(uint32_t wire_id, grpc_chttp2_setting_id* out);
@ -46,14 +47,15 @@ typedef enum {
GRPC_CHTTP2_DISCONNECT_ON_INVALID_VALUE
} grpc_chttp2_invalid_value_behavior;
struct grpc_chttp2_setting_parameters {
typedef struct {
const char* name;
uint32_t default_value;
uint32_t min_value;
uint32_t max_value;
grpc_chttp2_invalid_value_behavior invalid_value_behavior;
uint32_t error_value;
};
} grpc_chttp2_setting_parameters;
extern const grpc_chttp2_setting_parameters
grpc_chttp2_settings_parameters[GRPC_CHTTP2_NUM_SETTINGS];

@ -460,6 +460,10 @@ struct grpc_chttp2_transport
* thereby reducing the number of induced frames. */
uint32_t num_pending_induced_frames = 0;
bool reading_paused_on_pending_induced_frames = false;
/** Based on channel args, preferred_rx_crypto_frame_sizes are advertised to
* the peer
*/
bool enable_preferred_rx_crypto_frame_advertisement = false;
};
typedef enum {

@ -579,7 +579,7 @@ void PosixEndpointImpl::Read(absl::AnyInvocable<void(absl::Status)> on_read,
incoming_buffer_->Swap(last_read_buffer_);
read_mu_.Unlock();
if (args != nullptr && grpc_core::IsTcpFrameSizeTuningEnabled()) {
min_progress_size_ = args->read_hint_bytes;
min_progress_size_ = std::max(static_cast<int>(args->read_hint_bytes), 1);
} else {
min_progress_size_ = 1;
}

@ -1144,8 +1144,9 @@ static void tcp_read(grpc_endpoint* ep, grpc_slice_buffer* incoming_buffer,
tcp->read_cb = cb;
tcp->read_mu.Lock();
tcp->incoming_buffer = incoming_buffer;
tcp->min_progress_size =
grpc_core::IsTcpFrameSizeTuningEnabled() ? min_progress_size : 1;
tcp->min_progress_size = grpc_core::IsTcpFrameSizeTuningEnabled()
? std::max(min_progress_size, 1)
: 1;
grpc_slice_buffer_reset_and_unref(incoming_buffer);
grpc_slice_buffer_swap(incoming_buffer, &tcp->last_read_buffer);
TCP_REF(tcp, "read");

@ -18,18 +18,71 @@
#include "gtest/gtest.h"
#include <grpc/grpc.h>
#include <grpc/support/time.h>
#include "src/core/lib/experiments/experiments.h"
#include "src/core/lib/gpr/useful.h"
#include "src/core/lib/gprpp/ref_counted_ptr.h"
#include "src/core/lib/gprpp/time.h"
#include "src/core/lib/iomgr/exec_ctx.h"
#include "src/core/lib/resource_quota/resource_quota.h"
#include "src/core/lib/transport/bdp_estimator.h"
extern gpr_timespec (*gpr_now_impl)(gpr_clock_type clock_type);
namespace grpc_core {
namespace chttp2 {
namespace {
constexpr uint64_t kMaxAdvanceTimeMillis = 24ull * 365 * 3600 * 1000;
auto* g_memory_owner = new MemoryOwner(
ResourceQuota::Default()->memory_quota()->CreateMemoryOwner("test"));
gpr_timespec g_now;
gpr_timespec now_impl(gpr_clock_type clock_type) {
GPR_ASSERT(clock_type != GPR_TIMESPAN);
gpr_timespec ts = g_now;
ts.clock_type = clock_type;
return ts;
}
void InitGlobals() {
g_now = {1, 0, GPR_CLOCK_MONOTONIC};
TestOnlySetProcessEpoch(g_now);
gpr_now_impl = now_impl;
}
void AdvanceClockMillis(uint64_t millis) {
ExecCtx exec_ctx;
g_now = gpr_time_add(g_now, gpr_time_from_millis(Clamp(millis, uint64_t(1),
kMaxAdvanceTimeMillis),
GPR_TIMESPAN));
exec_ctx.InvalidateNow();
}
class TransportTargetWindowEstimatesMocker
: public chttp2::TestOnlyTransportTargetWindowEstimatesMocker {
public:
explicit TransportTargetWindowEstimatesMocker() {}
double ComputeNextTargetInitialWindowSizeFromPeriodicUpdate(
double current_target) override {
const double kSmallWindow = 16384;
const double kBigWindow = 1024 * 1024;
// Bounce back and forth between small and big initial windows.
if (current_target > kSmallWindow) {
return kSmallWindow;
} else {
return kBigWindow;
}
}
};
} // namespace
TEST(FlowControl, NoOp) {
ExecCtx exec_ctx;
TransportFlowControl tfc("test", true, g_memory_owner);
@ -38,6 +91,7 @@ TEST(FlowControl, NoOp) {
EXPECT_EQ(tfc.acked_init_window(), 65535);
EXPECT_EQ(tfc.remote_window(), 65535);
EXPECT_EQ(tfc.target_frame_size(), 16384);
EXPECT_EQ(tfc.target_preferred_rx_crypto_frame_size(), INT_MAX);
EXPECT_EQ(sfc.remote_window_delta(), 0);
EXPECT_EQ(sfc.min_progress_size(), 0);
EXPECT_EQ(sfc.announced_window_delta(), 0);
@ -47,12 +101,16 @@ TEST(FlowControl, SendData) {
ExecCtx exec_ctx;
TransportFlowControl tfc("test", true, g_memory_owner);
StreamFlowControl sfc(&tfc);
int64_t prev_preferred_rx_frame_size =
tfc.target_preferred_rx_crypto_frame_size();
{
StreamFlowControl::OutgoingUpdateContext sfc_upd(&sfc);
sfc_upd.SentData(1024);
}
EXPECT_EQ(sfc.remote_window_delta(), -1024);
EXPECT_EQ(tfc.remote_window(), 65535 - 1024);
EXPECT_EQ(tfc.target_preferred_rx_crypto_frame_size(),
prev_preferred_rx_frame_size);
}
TEST(FlowControl, InitialTransportUpdate) {
@ -70,15 +128,52 @@ TEST(FlowControl, InitialStreamUpdate) {
FlowControlAction());
}
TEST(FlowControl, PeriodicUpdate) {
ExecCtx exec_ctx;
TransportFlowControl tfc("test", true, g_memory_owner);
constexpr int kNumPeriodicUpdates = 100;
Timestamp next_ping = Timestamp::Now() + Duration::Milliseconds(1000);
uint32_t prev_max_frame_size = tfc.target_frame_size();
for (int i = 0; i < kNumPeriodicUpdates; i++) {
BdpEstimator* bdp = tfc.bdp_estimator();
bdp->AddIncomingBytes(1024 + (i * 100));
// Advance clock till the timestamp of the next ping.
AdvanceClockMillis((next_ping - Timestamp::Now()).millis());
bdp->SchedulePing();
bdp->StartPing();
AdvanceClockMillis(10);
next_ping = bdp->CompletePing();
FlowControlAction action = tfc.PeriodicUpdate();
if (IsTcpFrameSizeTuningEnabled()) {
if (action.send_max_frame_size_update() !=
FlowControlAction::Urgency::NO_ACTION_NEEDED) {
prev_max_frame_size = action.max_frame_size();
}
EXPECT_EQ(action.preferred_rx_crypto_frame_size(),
Clamp(2 * prev_max_frame_size, 16384u, 0x7fffffffu));
EXPECT_TRUE(action.preferred_rx_crypto_frame_size_update() !=
FlowControlAction::Urgency::NO_ACTION_NEEDED);
} else {
EXPECT_EQ(action.preferred_rx_crypto_frame_size(), 0);
EXPECT_TRUE(action.preferred_rx_crypto_frame_size_update() ==
FlowControlAction::Urgency::NO_ACTION_NEEDED);
}
}
}
TEST(FlowControl, RecvData) {
ExecCtx exec_ctx;
TransportFlowControl tfc("test", true, g_memory_owner);
StreamFlowControl sfc(&tfc);
StreamFlowControl::IncomingUpdateContext sfc_upd(&sfc);
int64_t prev_preferred_rx_frame_size =
tfc.target_preferred_rx_crypto_frame_size();
EXPECT_EQ(absl::OkStatus(), sfc_upd.RecvData(1024));
sfc_upd.MakeAction();
EXPECT_EQ(tfc.announced_window(), 65535 - 1024);
EXPECT_EQ(sfc.announced_window_delta(), -1024);
EXPECT_EQ(tfc.target_preferred_rx_crypto_frame_size(),
prev_preferred_rx_frame_size);
}
TEST(FlowControl, TrackMinProgressSize) {
@ -170,5 +265,8 @@ TEST(FlowControl, GradualReadsUpdate) {
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
grpc_core::chttp2::g_test_only_transport_target_window_estimates_mocker =
new grpc_core::chttp2::TransportTargetWindowEstimatesMocker();
grpc_core::chttp2::InitGlobals();
return RUN_ALL_TESTS();
}

@ -50,10 +50,12 @@ _SETTINGS = {
clamp_invalid_value),
'GRPC_ALLOW_TRUE_BINARY_METADATA':
Setting(0xfe03, 0, 0, 1, clamp_invalid_value),
'GRPC_PREFERRED_RECEIVE_CRYPTO_FRAME_SIZE':
Setting(0xfe04, 0, 16384, 0x7fffffff, clamp_invalid_value),
}
H = open('src/core/ext/transport/chttp2/transport/http2_settings.h', 'w')
C = open('src/core/ext/transport/chttp2/transport/http2_settings.c', 'w')
C = open('src/core/ext/transport/chttp2/transport/http2_settings.cc', 'w')
# utility: print a big comment block into a set of files
@ -91,14 +93,15 @@ print("#ifndef GRPC_CORE_EXT_TRANSPORT_CHTTP2_TRANSPORT_HTTP2_SETTINGS_H",
print("#define GRPC_CORE_EXT_TRANSPORT_CHTTP2_TRANSPORT_HTTP2_SETTINGS_H",
file=H)
print(file=H)
print("#include <grpc/support/port_platform.h>", file=H)
print("#include <stdint.h>", file=H)
print("#include <stdbool.h>", file=H)
print(file=H)
print("#include <grpc/support/port_platform.h>", file=C)
print("#include \"src/core/ext/transport/chttp2/transport/http2_settings.h\"",
file=C)
print(file=C)
print("#include <grpc/support/useful.h>", file=C)
print("#include \"src/core/lib/gpr/useful.h\"", file=C)
print("#include \"src/core/lib/transport/http2_errors.h\"", file=C)
print(file=C)
@ -162,7 +165,7 @@ for i, r in enumerate(p.r):
print('case %d: h += %d; break;' % (i, r), file=C)
print("""
}
*out = (grpc_chttp2_setting_id)h;
*out = static_cast<grpc_chttp2_setting_id>(h);
return h < GPR_ARRAY_SIZE(grpc_setting_id_to_wire_id) && grpc_setting_id_to_wire_id[h] == wire_id;
}
""" % cgargs,

Loading…
Cancel
Save