Skip to content

Commit 0bbef7b

Browse files
committed
Merge main into sync PR 79503
Signed-off-by: luohaha <18810541851@163.com>
2 parents 443f90f + 09c9186 commit 0bbef7b

73 files changed

Lines changed: 3747 additions & 401 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

be/src/base/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ ADD_BE_LIB(Base
8383
time/monotime.cpp
8484
time/time.cpp
8585
time/timezone_utils.cpp
86+
time/tz_offset_cache.cpp
8687
template/mustache/mustache.cc
8788
# base/testutil is not the UT-only TestUtil library. Production sources
8889
# include its BE_TEST-gated sync-point/fault-injection macros, which compile
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright 2021-present StarRocks, Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "base/time/tz_offset_cache.h"
16+
17+
#include <algorithm>
18+
#include <chrono>
19+
#include <limits>
20+
21+
namespace starrocks {
22+
23+
namespace {
24+
// civil_second interpreted as if it were UTC, in whole seconds since the Unix epoch. Used only
25+
// as an offset-free anchor for the additive identity civil = epoch + (unix_seconds + tz_offset);
26+
// not itself a valid timezone conversion.
27+
int64_t civil_as_utc_seconds(const cctz::civil_second& cs) {
28+
return cs - cctz::civil_second(1970, 1, 1, 0, 0, 0);
29+
}
30+
31+
// Past the last transition in a zone's explicit table, cctz's next_transition() returns false
32+
// unconditionally ("ignoring future_spec_", per its own doc comment) -- it cannot tell us whether
33+
// that's because the zone will hold `offset` forever (e.g. Asia/Shanghai and Asia/Kolkata, which
34+
// abolished DST decades ago, or America/Phoenix, which has been fixed MST since it was defined --
35+
// all of them reach this point on ordinary present-day lookups, not just far-future ones) or
36+
// because it's a still-cycling DST zone we've simply run past the enumerable table for (only
37+
// reachable past ~year 2437 for this build's tzdata).
38+
//
39+
// There is no zone-specific logic here -- this is a generic probe applied to whatever zone was
40+
// passed in, and it works by exploiting a property of DST rules in general rather than anything
41+
// about a particular zone: every real-world annual DST cycle has exactly two transitions, and
42+
// each phase (DST / standard time) lasts several months, never mere days or weeks. So sampling
43+
// the offset at a few points spread roughly a quarter-year apart -- +91d, +182d, +273d, i.e.
44+
// ~3/6/9 months out, alongside `offset` itself already sampled at `tp` -- puts one sample in
45+
// each of the four seasons. If the zone is still cycling, at least one of those samples is
46+
// guaranteed to land in a different phase than `tp` and show a different offset; if it's
47+
// permanently fixed, all of them trivially match. The exact day counts aren't calibrated to any
48+
// zone's specific transition dates (that would be pointless -- the whole point is this has to
49+
// work for a zone we can no longer enumerate transitions for); they just need to be spaced closer
50+
// together than the shortest real-world DST phase, which they are by a wide margin.
51+
bool offset_is_constant_beyond(const cctz::time_zone& tz, const cctz::time_point<cctz::seconds>& tp, int64_t offset) {
52+
for (int64_t days : {91, 182, 273}) {
53+
if (tz.lookup_offset(tp + cctz::seconds(days * 86400)).offset != offset) {
54+
return false;
55+
}
56+
}
57+
return true;
58+
}
59+
} // namespace
60+
61+
int64_t TzOffsetCache::offset_for_unix(int64_t unix_sec, const cctz::time_zone& tz) {
62+
if (_abs_window.has_value && tz == _abs_window.zone && unix_sec >= _abs_window.lo && unix_sec < _abs_window.hi) {
63+
return _abs_window.offset;
64+
}
65+
66+
static const cctz::time_point<cctz::seconds> epoch =
67+
std::chrono::time_point_cast<cctz::seconds>(std::chrono::system_clock::from_time_t(0));
68+
const cctz::time_point<cctz::seconds> tp = epoch + cctz::seconds(unix_sec);
69+
_abs_window.offset = tz.lookup_offset(tp).offset;
70+
71+
int64_t lo = std::numeric_limits<int64_t>::min();
72+
int64_t hi = std::numeric_limits<int64_t>::max();
73+
cctz::time_zone::civil_transition ct;
74+
// next_transition(tp)/prev_transition(tp) both use a *strict* inequality against tp, so
75+
// probing prev_transition at tp+1s (rather than tp) is what makes it return "the largest
76+
// transition <= tp" instead of skipping over a transition that lands exactly on it.
77+
const bool has_prev = tz.prev_transition(tp + cctz::seconds(1), &ct);
78+
if (has_prev) {
79+
lo = tz.lookup(ct.to).trans.time_since_epoch().count();
80+
}
81+
const bool has_next = tz.next_transition(tp, &ct);
82+
if (has_next) {
83+
hi = tz.lookup(ct.to).trans.time_since_epoch().count();
84+
}
85+
if (has_prev && !has_next && !offset_is_constant_beyond(tz, tp, _abs_window.offset)) {
86+
// has_prev true but has_next false, and the offset actually varies later on: this is a
87+
// zone with an ongoing DST cycle that we've simply run past cctz's enumerable transition
88+
// table for (see offset_is_constant_beyond's comment). Caching an unbounded window here
89+
// would silently reuse this offset for arbitrarily-far future instants that could be in
90+
// the opposite DST season -- so don't cache; every such row gets a fresh, authoritative
91+
// lookup instead. When the offset turns out to be constant beyond this point (e.g.
92+
// Asia/Shanghai, Asia/Kolkata, America/Phoenix -- the common case this branch actually
93+
// hits on ordinary present-day lookups), fall through and cache with hi left at +inf.
94+
_abs_window.has_value = false;
95+
return _abs_window.offset;
96+
}
97+
_abs_window.zone = tz;
98+
_abs_window.lo = lo;
99+
_abs_window.hi = hi;
100+
_abs_window.has_value = true;
101+
return _abs_window.offset;
102+
}
103+
104+
int64_t TzOffsetCache::unix_for_civil(int64_t civil_as_utc_sec, int year, int month, int day, int hour, int minute,
105+
int second, const cctz::time_zone& tz) {
106+
if (_civil_window.has_value && tz == _civil_window.zone && civil_as_utc_sec >= _civil_window.lo &&
107+
civil_as_utc_sec < _civil_window.hi) {
108+
return civil_as_utc_sec - _civil_window.offset;
109+
}
110+
111+
// Cold path: only reached ~twice per DST transition (or once per call for a wildly
112+
// out-of-order stream), so it's fine to pay cctz::civil_second's construction/arithmetic
113+
// cost here -- unlike the hot path above, which never touches it.
114+
const cctz::civil_second cs(year, month, day, hour, minute, second);
115+
const cctz::time_zone::civil_lookup cl = tz.lookup(cs);
116+
const cctz::time_point<cctz::seconds> answer =
117+
(cl.kind == cctz::time_zone::civil_lookup::SKIPPED) ? cl.trans : cl.pre;
118+
const int64_t answer_unix = answer.time_since_epoch().count();
119+
120+
if (cl.kind != cctz::time_zone::civil_lookup::UNIQUE) {
121+
// Ambiguous wall-clock value (the skipped/repeated hour around a DST change, ~1h/year).
122+
// Too rare to be worth caching a window for; always re-resolve authoritatively.
123+
_civil_window.has_value = false;
124+
return answer_unix;
125+
}
126+
127+
cctz::time_zone::civil_transition prev_trans, next_trans;
128+
const bool has_prev = tz.prev_transition(answer + cctz::seconds(1), &prev_trans);
129+
const bool has_next = tz.next_transition(answer, &next_trans);
130+
131+
int64_t lo = std::numeric_limits<int64_t>::min();
132+
int64_t hi = std::numeric_limits<int64_t>::max();
133+
if (has_prev) {
134+
// For a spring-forward (gap) transition, prev_trans.to is later and starts the segment;
135+
// for a fall-back (repeat) transition, prev_trans.from is later, and cctz's "prefer pre"
136+
// tie-break means the whole repeated hour still belongs to the *earlier* segment, not
137+
// this one -- either way max(from, to) is where the current segment actually starts.
138+
lo = civil_as_utc_seconds(std::max(prev_trans.from, prev_trans.to));
139+
}
140+
if (has_next) {
141+
hi = civil_as_utc_seconds(std::min(next_trans.from, next_trans.to));
142+
}
143+
if (has_prev && !has_next && !offset_is_constant_beyond(tz, answer, civil_as_utc_sec - answer_unix)) {
144+
// Same "cctz gives up enumerating past the explicit table" hazard as in offset_for_unix
145+
// above, and the same disambiguation: only skip caching when the offset actually varies
146+
// beyond this point (a still-cycling DST zone run past the enumerable table), not when
147+
// it's simply a zone permanently fixed since its last historical transition (the common
148+
// case this branch hits on ordinary present-day lookups for zones like Asia/Shanghai).
149+
_civil_window.has_value = false;
150+
return answer_unix;
151+
}
152+
if (!(lo < hi)) {
153+
// Pre-existing degenerate/adjacent-transitions guard.
154+
_civil_window.has_value = false;
155+
return answer_unix;
156+
}
157+
158+
_civil_window.zone = tz;
159+
// civil_as_utc_sec is the caller's non-cctz computation of the same quantity
160+
// civil_as_utc_seconds(cs) would give; using it here (instead of recomputing via cctz) keeps
161+
// this assignment consistent with what the hot path above will compare against later.
162+
_civil_window.offset = civil_as_utc_sec - answer_unix;
163+
_civil_window.lo = lo;
164+
_civil_window.hi = hi;
165+
_civil_window.has_value = true;
166+
return answer_unix;
167+
}
168+
169+
} // namespace starrocks

be/src/base/time/tz_offset_cache.h

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2021-present StarRocks, Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#pragma once
16+
17+
#include <cctz/civil_time.h>
18+
#include <cctz/time_zone.h>
19+
20+
#include <cstdint>
21+
22+
namespace starrocks {
23+
24+
// Caches the UTC offset currently in force for a cctz::time_zone, so that repeated conversions
25+
// against nearby times can skip cctz's transition-table search.
26+
//
27+
// cctz::time_zone itself already keeps such a hint (TimeZoneInfo::local_time_hint_ /
28+
// time_local_hint_ in time_zone_info.cc), but it is a single mutable slot shared by *every*
29+
// caller of that zone process-wide: under any real concurrency it becomes a cross-core
30+
// cache-line ping-pong, and one thread's hint constantly evicts another's. This class holds the
31+
// same kind of window privately per caller instead.
32+
//
33+
// Not thread-safe and not meant to be shared: each execution thread (or, in exprs/, each
34+
// FunctionContext worker via FunctionContext::get_or_create_thread_state) should own its own
35+
// instance. A single instance is reused across an unbounded number of calls and automatically
36+
// adapts as the input strays outside its current window -- there is no reset needed between
37+
// unrelated calls to the *same* (offset semantics never change for a fixed IANA zone), but an
38+
// instance must not be reused across two different cctz::time_zone values without expecting a
39+
// cache miss on the first call after the switch (which is always handled correctly, just not
40+
// from the fast path).
41+
//
42+
// Real-world timestamp columns are almost always time-clustered within one scan batch, so most
43+
// consecutive calls land in the same window; for a fixed-offset zone (no DST, e.g. "+08:00") the
44+
// window degenerates to unbounded after the very first call, so every later call is a hit
45+
// regardless of the time range covered.
46+
class TzOffsetCache {
47+
public:
48+
// Returns the UTC offset (seconds east) in force for `tz` at absolute unix time `unix_sec`.
49+
int64_t offset_for_unix(int64_t unix_sec, const cctz::time_zone& tz);
50+
51+
// Interprets the wall-clock fields (year, month, day, hour, minute, second) as being in `tz`
52+
// and returns the absolute unix-second instant, matching cctz::convert(civil_second, tz)'s
53+
// tie-break exactly: a SKIPPED (nonexistent) civil time resolves to the transition instant, a
54+
// REPEATED (ambiguous) one resolves to the earlier ("pre") instant. Ambiguous inputs are
55+
// always re-resolved through the authoritative cctz lookup and are never cached (they are
56+
// rare -- at most ~1 hour per DST transition).
57+
//
58+
// `civil_as_utc_sec` is the same wall-clock fields reinterpreted as literal UTC seconds since
59+
// the epoch (i.e. what they'd mean with no timezone applied at all) -- the caller computes
60+
// this via a cheap, non-cctz calendar routine (e.g. TimestampValue::to_unix_second(), which
61+
// this class -- living in base/, below types/ -- cannot call itself) so the common
62+
// cache-hit path never has to touch cctz::civil_second at all. Passing a value inconsistent
63+
// with the other fields is a caller bug; it is only used verbatim, never re-derived.
64+
int64_t unix_for_civil(int64_t civil_as_utc_sec, int year, int month, int day, int hour, int minute, int second,
65+
const cctz::time_zone& tz);
66+
67+
private:
68+
// `zone` records which cctz::time_zone the window was computed for. An instance's (from, to)
69+
// pair is normally fixed for its whole lifetime, but this guards against a window silently
70+
// being reused after the caller switches to a different zone.
71+
struct AbsWindow {
72+
bool has_value = false;
73+
cctz::time_zone zone;
74+
int64_t offset = 0;
75+
int64_t lo = 0; // inclusive, unix seconds
76+
int64_t hi = 0; // exclusive, unix seconds
77+
} _abs_window;
78+
79+
// Bounds are in the same "civil fields reinterpreted as literal UTC seconds" domain as
80+
// `unix_for_civil`'s civil_as_utc_sec parameter, not cctz::civil_second -- comparing plain
81+
// int64s on the hot path avoids cctz::civil_second arithmetic (specifically
82+
// cctz::detail::impl::n_min and friends, the normalization routine backing civil_second's
83+
// arithmetic operators) entirely once the window is warm.
84+
struct CivilWindow {
85+
bool has_value = false;
86+
cctz::time_zone zone;
87+
int64_t offset = 0; // seconds east of UTC
88+
int64_t lo = 0; // inclusive, civil-as-utc seconds
89+
int64_t hi = 0; // exclusive, civil-as-utc seconds
90+
} _civil_window;
91+
};
92+
93+
} // namespace starrocks

be/src/compute_env/spill/data_stream.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ std::shared_ptr<SpillOutputDataStream> create_spill_output_stream(Spiller* spill
147147
}
148148

149149
Status DataTranster::transfer(workgroup::YieldContext& yield_ctx, RuntimeState* state, Serde* serde,
150-
const SpillOutputDataStreamPtr& output, const InputStreamPtr& input_stream) {
150+
const SpillOutputDataStreamPtr& output, const InputStreamPtr& input_stream,
151+
RuntimeProfile::Counter* merge_timer) {
151152
// read data from input stream and append to output stream
152153
bool need_aligned = state->spill_enable_direct_io();
153154
auto task_context = std::any_cast<SpillIOTaskContextPtr>(yield_ctx.task_context_data);
@@ -167,7 +168,13 @@ Status DataTranster::transfer(workgroup::YieldContext& yield_ctx, RuntimeState*
167168
RETURN_IF_YIELD(yield_ctx.need_yield);
168169
}
169170
DCHECK(input_stream->is_ready());
170-
auto chunk_st = input_stream->get_next(yield_ctx, read_ctx);
171+
// Charge the read itself -- for a compaction, key comparison and chunk assembly -- to the
172+
// caller's timer. The deserialize and read IO it feeds on happen in the restore task above
173+
// and keep their own counters, so the two do not overlap.
174+
auto chunk_st = [&]() {
175+
SCOPED_TIMER(merge_timer);
176+
return input_stream->get_next(yield_ctx, read_ctx);
177+
}();
171178
RETURN_IF(!chunk_st.status().is_ok_or_eof(), chunk_st.status());
172179
RETURN_IF(chunk_st.status().is_end_of_file(), Status::OK());
173180
RETURN_IF_ERROR(serde->serialize(state, read_ctx, std::move(chunk_st.value()), output, need_aligned));

be/src/compute_env/spill/data_stream.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#pragma once
1616

1717
#include "base/string/slice.h"
18+
#include "common/runtime_profile.h"
1819
#include "common/status.h"
1920
#include "compute_env/spill/spill_fwd.h"
2021
#include "runtime/runtime_state_fwd.h"
@@ -44,8 +45,12 @@ using InputStreamPtr = std::shared_ptr<SpillInputStream>;
4445
// This class will execution io tasks. make sure call it in io threads
4546
class DataTranster {
4647
public:
48+
// merge_timer, when non-null, is charged with the time spent pulling chunks out of input_stream
49+
// (for a compaction that is the multi-way merge). Callers whose work is not a compaction pass
50+
// nullptr rather than mislabeling it; ScopedTimer no-ops on a null counter.
4751
static Status transfer(workgroup::YieldContext& yield_ctx, RuntimeState* state, Serde* serde,
48-
const SpillOutputDataStreamPtr& output, const InputStreamPtr& input_stream);
52+
const SpillOutputDataStreamPtr& output, const InputStreamPtr& input_stream,
53+
RuntimeProfile::Counter* merge_timer = nullptr);
4954
};
5055

5156
} // namespace starrocks::spill

be/src/compute_env/spill/spill_components.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ Status RawSpillerWriter::yieldable_flush_task(workgroup::YieldContext& yield_ctx
119119
}
120120

121121
Status RawSpillerWriter::_spill_mem_table(workgroup::YieldContext& yield_ctx, const MemTablePtr& mem_table) {
122+
SCOPED_TIMER(_spiller->metrics().flush_mem_table_timer);
122123
auto io_task = std::any_cast<SpillIOTaskContextPtr>(yield_ctx.task_context_data);
123124
auto flush_ctx = std::static_pointer_cast<FlushContext>(io_task);
124125

@@ -144,6 +145,7 @@ Status RawSpillerWriter::_spill_mem_table(workgroup::YieldContext& yield_ctx, co
144145
}
145146

146147
Status RawSpillerWriter::_compact_mem_table(workgroup::YieldContext& yield_ctx) {
148+
SCOPED_TIMER(_spiller->metrics().compact_timer);
147149
auto io_task = std::any_cast<SpillIOTaskContextPtr>(yield_ctx.task_context_data);
148150
auto flush_ctx = std::static_pointer_cast<FlushContext>(io_task);
149151
// flush_ctx->output is not nullptr means the task is resumed from yield point
@@ -159,6 +161,9 @@ Status RawSpillerWriter::_compact_mem_table(workgroup::YieldContext& yield_ctx)
159161

160162
COUNTER_UPDATE(_spiller->metrics().compact_count, 1);
161163
COUNTER_UPDATE(_spiller->metrics().compact_block_count, block_groups.size());
164+
COUNTER_UPDATE(_spiller->metrics().compact_bytes_read,
165+
std::accumulate(block_groups.begin(), block_groups.end(), int64_t{0},
166+
[](int64_t sum, const auto& group) { return sum + group->data_size(); }));
162167

163168
flush_ctx->compact_input_num_rows =
164169
std::accumulate(block_groups.begin(), block_groups.end(), 0,
@@ -174,11 +179,12 @@ Status RawSpillerWriter::_compact_mem_table(workgroup::YieldContext& yield_ctx)
174179
options().sort_exprs, options().sort_desc));
175180
}
176181
auto st = DataTranster::transfer(yield_ctx, _runtime_state, _spiller->serde().get(), flush_ctx->output,
177-
flush_ctx->input_stream);
182+
flush_ctx->input_stream, _spiller->metrics().compact_merge_timer);
178183
RETURN_IF(!st.is_ok_or_eof(), st);
179184
RETURN_IF_YIELD(yield_ctx.need_yield);
180185
RETURN_IF_ERROR(flush_ctx->output->flush());
181186
flush_ctx->output.reset();
187+
COUNTER_UPDATE(_spiller->metrics().compact_bytes_written, flush_ctx->block_group->data_size());
182188
// On cancellation the transfer stops early, so the compacted block group holds fewer rows than
183189
// its input; only assert full compaction while the query is still running. Query cancel sets
184190
// the runtime state (which the transfer honors) but not necessarily the spiller flag.

0 commit comments

Comments
 (0)