Skip to content

[seekdb][share] Avoid idle internal table polling - #1252

Open
hnwyllmm wants to merge 401 commits into
master.bakfrom
task/2026080300117949904
Open

[seekdb][share] Avoid idle internal table polling#1252
hnwyllmm wants to merge 401 commits into
master.bakfrom
task/2026080300117949904

Conversation

@hnwyllmm

@hnwyllmm hnwyllmm commented Aug 4, 2026

Copy link
Copy Markdown
Member

Task Description

Freeze, Timezone, DBMS Job, and DBMS Scheduler periodically query internal tables even when the tables have not changed. These idle reads continuously consume CPU and cache resources.

Solution Description

Maintain a process-local change sequence for each registered internal table. Transaction commit increments the corresponding sequence, while consumers keep their existing timers and skip internal SQL when the captured sequence is unchanged.
Seal the table registry after all modules finish initialization and before consumer timers start. Keep fail-open refresh semantics when sequence lookup fails. DBMS Job and DBMS Scheduler use full reconciliation on table changes and preserve retry/self-healing behavior for transient scheduling failures.

Passed Regressions

  • git diff --check
  • ob-make seekdb
  • ob-make test_internal_table_change_notifier
  • test_internal_table_change_notifier: 4/4 passed
  • seekdb startup smoke: notifier sealed with 9 tables and server startup reached 14/14

Upgrade Compatibility

Compatible. The change sequence is process-local and introduces no persistent data-format or upgrade change.

Other Information

  • This MR contains one commit: e906c59d6f0 optimize: avoid idle inner table polling.
  • SQLite-backed refresh_merge_info remains periodically refreshed and is not gated by the internal-table sequence.
  • No background thread pool experiment is included.
  • Related internal link: DIMA-2026080300117949904

Release Note

footka added 30 commits June 10, 2026 14:27
obperf sampling shows that KVCache Wash accounts for 61.4% of process CPU during seekdb idle state, making it the biggest hotspot. Each time the wash timer (200ms) triggers, it executes:
1. `ObKVCacheStore::wash`
   - `refresh_score`: Iterates through all mb_handles, performing hazptr protect/release for each one.
   - Heap construction loop: Iterates through all mb_handles again, performing hazptr protect/release for each one.
   → This results in two passes, with each handle undergoing protect/release twice.

2. `ObKVCacheMap::clean_garbage_node`
   - Iterates over 200K buckets, acquiring a write lock (`ObBucketWLockGuard`) for each.
   - During idle, the vast majority of buckets are empty, so locks are acquired and immediately released, which is pure waste.
   → This accounts for 88% of the total wash CPU (~1280/1407 samples are in `BucketLock::wrlock/unlock`).

## Optimization 1: Merged `refresh_score` into the heap construction loop (`ob_kvcache_store.cpp`)
The per-handle score decay logic from `refresh_score` was inlined into the existing heap construction loop.
Each handle now performs hazptr protect/release only once, down from twice.
The O(1) global calculation for `base_mb_score_` is preserved.

## Optimization 2: Fast skip for empty buckets (`ob_kvcache_map.cpp`)
Before acquiring a lock, `clean_garbage_node` and `replace_fragment_node` now check if `get_bucket_node(i)` is NULL.
- NULL → Skip, do not acquire the lock.
- Non-NULL → Acquire the lock, then double-check before processing.
Safety: Pointer reads are hardware-atomic on aarch64/x86_64; worst-case behavior reverts to the old logic.

- Before optimization (PID 100137, CPU 10.3%): Wash CPU 61.4%, `clean_garbage` 58.7%.
- Intermediate (PID 43062, only Optimization 1): Wash CPU 64.0%, `clean_garbage` 61.9% (Optimization 1 did not address the main bottleneck).
- After optimization (PID 98980, CPU 10.7%): Wash CPU 11.5%, `clean_garbage` 7.3% (↓81%).
- Functional verification: After forcing cache eviction, `clean_node_count=2~5`, and garbage nodes were correctly cleaned.

## Flame Graphs
- Before optimization: http://obperf.oceanbase-dev.com/files/profile_20260517004356__work.flame.svg
- After optimization: http://obperf.oceanbase-dev.com/files/profile_20260517100310__work.flame.svg
Background: obperf CPU sampling shows that in an idle seekdb instance, ObBKGDSessInActiveGuard accounts for 1.7% of samples (687/41161). This object is constructed/destructed each sleep cycle, each time involving:
- thread_local diagnostic info access
- reading the global variable is_ash_enabled
- set_sess_inactive: rdtsc + CACHE_ALIGNED bool write
- set_sess_active: rdtsc + idle time accumulation + trace_id read + ASH buffer binding

A full lifecycle is triggered 100 times per second on the TimeWheel deadlock detector (10ms precision).

Changes: Remove two instances of ObBKGDSessInActiveGuard.
1. ob_clock_generator.h: ObClockGenerator::usleep
   - Remove the inactive_guard construction before each nanosleep.
   - Main beneficiary: The TimeWheel stack guard in TimeWheelBase::scan goes from 303 samples to zero.
2. utility.h: ob_usleep(v, is_idle_sleep=true)
   - Remove the guard in the is_idle_sleep branch, merging both branches into a direct ob_usleep(v).
   - The is_idle_sleep parameter is kept but commented as unused for signature compatibility.
   - Beneficiaries: ObMultiTenant::run1, ObTimerService::run1, ObBaseLogWriter::do_flush_log, ObDDLTransController::run1, etc.

Results:
| Metric | Before Optimization | After Optimization |
|-----------------|-------------|-------------|
| BKGDSessGuard | 687 (1.7%) | 320 (0.7%) |
| TimeWheel+Guard | 303 samples | 0 samples |

The remaining Guard samples come from ~35 modules constructing it directly, not via the generic sleep functions.

Risk Assessment:
- The ASH sampler will record fewer inactive state switches during sleep periods.
- No functional impact on user-visible diagnostic information.
- The thread's ASH inactive flag can still be used by directly constructing the Guard.

Next Steps:
- The remaining 320 Guard samples come from direct construction in various modules (ob_timer_service, ob_base_log_writer, etc.) and can be optimized module-by-module as needed.
…e RTTI overhead.

On the server side, run_wrapper always points to an ObTenantBase instance, making the type check performed by dynamic_cast provably redundant. Switching to static_cast removes the virtual table traversal that occurs with every timer task during tenant switching.
…Mgr Timer Task.

ObPxTargetMgr executes `refresh_statistics` every 500ms. Within this, it uses the call chain `get_dummy_leader` -> `check_dummy_location_credible` -> `get_role` to determine if the current node is a Leader or Follower. Internally, `get_role` performs MTL_SWITCH + check_palf_exist + open_palf + get_role to query the Paxos role of SYS_LS. This is a CPU hotspot per tick (flame graph shows runTimerTask at 0.57%, with lib_mtl_switch accounting for 10.04% of that).

SeekDB is an observer-lite single-replica mode. `ElectionImpl::get_role` always returns LEADER, and the Leader never switches. This entire Leader/Follower coordination mechanism is effectively dead code.

## Changes Made

Removed the following dead code (-237 lines):
1. `get_dummy_leader` — MTL_SWITCH + nonblock_get_leader to locate the Leader.
2. `check_dummy_location_credible` — Calls `get_role` to verify cache credibility.
3. `get_role` — MTL_SWITCH + open_palf to get the Paxos role.
4. `refresh_dummy_location` — Empty function, only returns OB_SUCCESS.
5. `query_statistics` — RPC logic for Followers to report resource increments to the Leader.
6. `reset_follower_statistics` — Resets statistics on the Follower side.

Removed member variables that are no longer needed:
- `cluster_id_`, `dummy_cache_leader_`, `rpc_proxy_`, `need_send_refresh_all_`

Simplified `refresh_statistics`:
- Removed the `get_dummy_leader` call and the Follower branch.
- On the first tick, executes `reset_leader_statistics` once; subsequent ticks return directly.

This change applies only to single-replica (observer-lite) deployments. Multi-machine OceanBase clusters, which have a genuine need for Leader/Follower switching, cannot use this modification.

- Compilation passes.
- OBD deployment of a single-node instance starts normally.
- MySQL connections work normally.
…n-memory atomic CAS

## Problem
The Fetcher main loop (ObCSFetcher::run1) calls try_advance_refresh_scn_ every 200ms
unconditionally, regardless of IDLE or ACTIVE mode. Under IDLE mode (no async vector
index tables), the loop iterates every 10ms, and get_refresh_scn returns GTS each time.
This results in a SQL UPDATE against __all_global_stat every 200ms even when the value
has not changed, causing unnecessary database writes.

Additionally, the Worker path (do_finish_batch_) also writes refresh_scn via SQL
within a transaction, and the virtual table query reads it via SQL — all incurring
database round-trips for a monotonically-advancing counter that only needs to be
queryable within the same process.

## Solution
Migrate refresh_scn from __all_global_stat (SQL-persisted) to in-memory management
in ObCSDispatcher using atomic variables (ATOMIC_LOAD/ATOMIC_STORE/ATOMIC_BCAS)
1. ObCSDispatcher::refresh_scn_ becomes an atomic variable with CAS-based
update_refresh_scn that advances the value only when the new value is larger.

2. Fetcher path (try_advance_refresh_scn_)
- Was: ObGlobalStatProxy::advance_change_stream_refresh_scn → SQL UPDATE
- Now: dispatcher_->update_refresh_scn → in-memory CAS, zero SQL

3. Worker path (do_finish_batch_)
- refresh_scn advancement moves from release_batch to after batch commit
- Uses dispatcher_->update_refresh_scn instead of SQL

4. Read paths
- wait_refresh_scn in ObChangeStreamMgr reads from dispatcher_->get_refresh_scn
- Virtual table ob_all_virtual_change_stream_refresh_stat reads in-memory state
- init_refresh_scn_ still loads from __all_global_stat once as recovery baseline
and allows rollback on reload (recovery semantics differ from runtime advancement)

5. Cleanup
- Removes refresh_scn_inited flag and init_refresh_scn public method
- Refactors release_batch to no longer manage refresh_scn directly

## Files Changed
- ob_change_stream_dispatcher.{h,cpp}: atomic refresh_scn_, update_refresh_scn, cleanup
- ob_change_stream_fetcher.cpp: Fetcher uses dispatcher->update_refresh_scn
- ob_change_stream_worker.cpp: Worker commits refresh_scn after batch success
- ob_change_stream_mgr.cpp: wait_refresh_scn reads from in-memory dispatcher
- ob_all_virtual_change_stream_refresh_stat.cpp: virtual table reads from memory
- wait_cs_sync.inc: mysqltest adjustment
…anual concatenation using MEMCPY.

set_ext_tname is called each time a timer task executes (a hot path in handle). The original implementation used databuff_printf to format the thread name as "%s_%s", incurring runtime overhead from format string parsing and va_list variable arguments.

Replaced databuff_printf with manual concatenation using STRLEN + MEMCPY: first, get the length of both strings, check that the total length does not exceed OB_EXTENED_THREAD_NAME_BUF_LEN (32), then sequentially memcpy the tname, '_', timer_name_, and '\0'.

- tname (OB_THREAD_NAME_BUF_LEN=16) and timer_name_ (16) each have ≤15 valid characters. After concatenation, the total is ≤31, which always satisfies the boundary check condition of <32.
- The MEMCPY / STRLEN macros are indirectly introduced via ob_define.h, adding no new dependencies.

- Full compilation passes with 0 errors.
- After starting a new instance, compared the log output for ext_tname; the format is completely consistent with the old implementation (e.g., TimerWK0_KVCacheWash, TimerWK0_AdvanceCKPT).
The obperf flame graph shows BlockGCTimerTask::runTimerTask consuming 3.31% of CPU, with two calls to palf_handle_impl_map_.for_each (for `get_total_used_disk_space_` and `recycle_blocks_`) each going through the ObLinkHashMap Iterator/HandleOn/revert path. In this environment, there is only 1 sys LS with a fixed palf_id of ObLSID::SYS_LS_ID (=1), making the for_each traversal entirely wasteful.

Changes:
1. `get_total_used_disk_space_` — Directly call `get_palf_handle_impl(SYS_LS_ID, guard)` instead of `for_each(GetTotalUsedDiskSpace functor)`.
2. `recycle_blocks_` — Directly call `get_palf_handle_impl(SYS_LS_ID, guard)` and inline the recycling logic (base_lsn check, block GC condition check, delete_block) instead of `for_each(LogGetRecycableFileCandidate functor)`.
3. Remove the now-unused `GetTotalUsedDiskSpace` and `LogGetRecycableFileCandidate` functors (both declarations and implementations).
4. Add `#include "share/ob_ls_id.h"`.
OB_ERR_EMPTY_QUERY means the SRS table has not been fully imported
(srs_cnt < 5152), so retrying is pointless — wait for the import
notifier to trigger a fresh refresh instead of busy-looping every 1s.
The MemoryDump thread previously triggered a STAT_LABEL scan every 10
seconds, iterating all tenants * ctx_ids * chunks * blocks * objects
to generate per-label memory statistics. This periodic full-memory walk
caused significant CPU overhead on production machines.

This commit sets STAT_LABEL_INTERVAL to INT64_MAX, effectively disabling
periodic auto-scanning. The original code path is fully preserved.

To manually trigger a memory stat scan
ALTER SYSTEM REFRESH MEMORY STAT;

Other on-demand dump triggers (kill -62 signal, etc/dump.config) are
unaffected and continue to work normally.

The STAT_LABEL_INTERVAL constant remains as a single point for future
standardization — when a configurable interval parameter is introduced
this constant should be replaced with that parameter.
…age/update_queue_size, and relax TIME_SLICE_PERIOD from 10ms to 1s.

Flame graph analysis (obperf, 155,396 total samples) of the 10ms scheduling loop in ObMultiTenant::run1 shows two types of unnecessary periodic overhead in ObTenant::timeup.

1. update_token_usage — Every 1 second, it traverses the worker list, atomically clears idle_us_, and calculates token_usage_. However, token_usage_ and worker_us_ are only read by virtual tables (ob_all_virtual_sys_stat / ob_all_virtual_res_mgr_sys_stat) for display and do not affect any scheduling decisions.

2. update_queue_size — Every 10ms, it calls ObServerConfig::get_instance to read the tenant_task_queue_size configuration item. Under normal conditions, this configuration doesn't change, making this an ineffective polling operation.

3. TIME_SLICE_PERIOD = 10000 (10ms) — The flame graph shows ObTenant::timeup itself consumes 3.49% of total CPU, with lock operations (~47%), retry queue processing (32%), and worker recycling (19%). Being triggered every 10ms is too frequent. Relaxing this to 1s (1000000us) can significantly reduce lock contention overhead, and the semantics of tenant inspection (worker start/stop, retry replay) are not sensitive to millisecond-level delays.

- Delete the update_token_usage method body and remove its call in timeup.
- Delete the update_queue_size method body and remove its call in timeup.
- Change get_token_usage and get_worker_time to directly return 0 (preserving interface compatibility for virtual tables).
- Remove member variables: token_usage_, token_usage_check_ts_, worker_us_.
- Replace update_queue_size with an inline set_queue_limit(int64_t) that directly operates on req_queue_.
- Remove the corresponding members from the constructor's initialization list.

- TIME_SLICE_PERIOD: 10000 → 1000000 (10ms → 1s).
- Add reload_tenant_task_queue_size: acquire a read lock, read GCONF.tenant_task_queue_size, and call tenant_->set_queue_limit.
- Timing interval for dumping tenant information: 10s → 1s (improves monitoring granularity).

- Add a call to reload_tenant_task_queue_size at the end of ObServerReloadConfig::operator, piggybacking on the server-level configuration hot-reload path.

- tenant_task_queue_size will no longer be polled every 10ms. Instead, it will be updated passively when triggered by ALTER SYSTEM SET.
Configuration change path: RPC → config_mgr_->reload_config → ObServerReloadConfig::operator → ObReloadConfig::operator updates GCONF → reload_tenant_task_queue_size → set_queue_limit(GCONF.tenant_task_queue_size).

- The output of update_token_usage was only used for display. After removal, virtual table queries will return 0, which does not affect system behavior.

- Relaxing TIME_SLICE_PERIOD from 10ms to 1s reduces the timeup call frequency from 100Hz to 1Hz, directly cutting down the cumulative overhead from lock contention, retry drain, and worker traversal.
## Background
ObBGThreadMonitor is a thread-level function execution timeout watchdog
designed to monitor background thread function execution time. It was
fully initialized and running (consuming ~0.52% CPU per flame graph
analysis), but **zero** business code was instrumented with MonitorGuard —
no threads or functions were ever registered for monitoring.

Flame graph data (155,396 total samples)
ObBGThreadMonitorTimerTask::runTimerTask: 805 samples (0.52%)
— all self-samples, spent scanning 500 empty MonitorEntryStack arrays
every second (ObClockGenerator::getClock + 2500 timestamp checks
+ spinlock acquire/release per cycle).

## Changes
### Deleted
- src/share/ob_bg_thread_monitor.h — all classes: MonitorGuard, ObBGThreadMonitor
ObBGThreadMonitorTimerTask, MonitorEntryStack, MonitorEntry
BGDummyCallback, IBGCallback, MonitorCallbackWrapper
ObTSIBGMonitorMemory, ObTSIBGMonitorSlotInfo, macros BG_MONITOR_GUARD(_DEFAULT)
BG_NEW_CALLBACK, BG_DELETE_CALLBACK
- src/share/ob_bg_thread_monitor.cpp — all implementations

### Modified
- src/observer/ob_server.cpp
removed #include, init/start/stop/wait/destroy lifecycle calls
- src/share/ob_thread_define.h
removed TG_DEF(BGThreadMonitor, BGThreadMonitor, TIMER)
- src/share/CMakeLists.txt
removed ob_bg_thread_monitor.cpp from build

## Risk Assessment
- Zero-risk: no business code references MonitorGuard/BG_MONITOR_GUARD.
Search across entire src/ confirmed no includes of ob_bg_thread_monitor.h
outside the deleted files and ob_server.cpp lifecycle management.
- No test files reference the framework.

## Verification
- Debug build: passed (make -j80, [100%] Built target observer)
- Instance deployment: deployed to ~/ob1, startup clean, SQL connectivity OK
Problem
ObCSDispatcher::run1 uses dispatch_cond_.wait(100) in its idle loop.
Each wakeup triggers futex kernel operations + ObWaitEventGuard +
ObDiagnosticInfo::end_wait_event accounting, creating ~10 scheduling
events per second even when there is no work. Flame graph (alloc view)
shows this accounts for 0.95% of system-wide allocation events.

Analysis
The condition variable is correctly used with the standard pattern
1. Lock mutex (ObThreadCondGuard)
2. Check condition under lock
3. cond.wait(timeout)
Signal is sent under the same mutex in push, so POSIX guarantees
that a waiting thread is woken immediately regardless of timeout.
The timeout is purely a fallback; longer timeouts do not risk losing
signals.

Solution
1. Increase dispatch_cond_.wait(100) to dispatch_cond_.wait(10000)
idle wakeups drop from 10/sec to 0.1/sec, reducing alloc overhead
by ~100x in the idle path.
2. Add dispatch_cond_.signal in stop: prevents the 10s timeout
from delaying shutdown when the thread is blocked in wait.

Verification
- Deployed binary, confirmed idle wakeup intervals are exactly 10s
(wait_duration_us = 10000067, 10000155, 10000087, 10000078).
- Measured shutdown latency: 168ms (signal in stop works correctly).
- Push→signal still wakes dispatcher immediately per condvar
semantics (requires change stream activity to exercise directly).
…o a raw pointer.

seekdb (lite deployment) always has only one LS (SYS_PALF_ID=1), which is never deleted after creation.
The original ObLinkHashMap<LSKey, IPalfHandleImpl> was designed for multi-LS scenarios. Every access had to go through hash map lookup, value allocation, and reference counting, which is pure overhead in the single-LS case.

Profiling revealed that LogLoopThread consumes a significant number of CPU cycles in the lambda within for_each, while in reality it's just making a simple method call on the single PalfHandleImpl.

### Data Structures (palf_env_impl.h/.cpp)
- Remove the PalfHandleImplAlloc class and its 4 methods.
- Remove the typedef ObLinkHashMap<LSKey, IPalfHandleImpl, PalfHandleImplAlloc>.
- Remove `#include "lib/hash/ob_link_hashmap.h"`.
- Replace PalfHandleImplMap + sys_ls_handle_ with `IPalfHandleImpl *single_palf_handle_`.

### Remove Reference Counting
- `revert_palf_handle_impl`: No-op (handle lifetime == process lifetime).
- `wait_until_reference_count_to_zero_`: Directly returns OB_SUCCESS.
- `create_palf_handle_impl_`: Assigns the raw pointer.
- `remove_palf_handle_impl_from_map_not_guarded_by_lock_`: Directly free + set to null.

### Simplify get_palf_handle_impl
Old: hash map lookup → ref inc → check_can_be_used → revert on failure.
New: null check → check_can_be_used → directly return the pointer.

### Simplify for_each (two overloads)
Old: ObLinkHashMap::for_each + lambda wrapper.
New: get_palf_handle_impl(SYS_PALF_ID, handle) → func → revert.

### Simplify Callers
- LogLoopThread: 4 for_each calls → 4 direct method calls, removing all lambdas.
- LogUpdater: for_each → directly call update_palf_stat.
- `get_total_used_disk_space_`: map for_each → functor(ls_key, single_palf_handle_).
- `recycle_blocks_`: map for_each → directly call `single_palf_handle_->delete_block`.
- `check_can_create_palf_handle_impl_`: map.count → null check.
- `check_can_update_log_disk_options_`: map.count → null check.

### Memory Safety Invariant
`single_palf_handle_` is set once at startup and only set to null during shutdown. It is never freed during operation.
Threads reading the pointer do not require synchronization. The `palf_meta_lock_` (RWLock) ensures writes during create/reload are visible to all subsequent reads.
…mand

## Problem
ObDiskUsageReportTask::runTimerTask runs periodically to refresh an
in-memory cache (result_map_) that maps (file_type, tenant_id) -> disk usage.
Flame graph analysis (profile_20260520115549) shows this consumes ~1.11% CPU
runTimerTask 314 samples (1.11%)
count_tenant 314 self: 3
count_tenant_data 311 self: 1
get_next_tablet_ptr 310 self: 0
fetch_tablet_item 310 self: 0
for_each_value 310 self: 0
try_lock_all 310 self: 18
try_rdlock 292 (1.03%) self: 292 <-- hotspot

94% of the cost is ObLatch::try_rdlock atomic CAS operations -
try_lock_all iterates ~1281 latches (10243 buckets / 8) and atomically
try_rdlock's each one. This runs on every timer tick even when nobody
reads the result.

## Analysis of consumers
The result_map_ cache has exactly two consumers
1. get_data_disk_used_size - serves __all_virtual_unit virtual table
2. delete_tenant_usage_stat - called on tenant drop (N/A for seekdb)

There is NO SQL reporting path. The comment in report_tenant_disk_usage
says "update the usage table" but copies result_map_ to a local array and
discards it - the reporting code was never implemented.

For seekdb (single tenant, never deleted), the timer is pure overhead
the only reader of result_map_ is the virtual table query.

## Solution
Stop the DiskUseReport timer entirely, and make get_data_disk_used_size
compute disk usage synchronously on each query. All timer-related code
is deleted (not commented out)
### ob_disk_usage_reporter.h
- Remove ObTimerTask inheritance (class no longer acts as a timer)
- Remove runTimerTask, report_tenant_disk_usage, refresh_tenant_disk_usage
count_tenant, count_tenant_slog, count_tenant_clog, count_server_slog
count_server_clog, count_server_meta, count_tenant_tmp declarations
- Remove ObReportResultGetter helper class
- Remove const from get_data_disk_used_size

### ob_disk_usage_reporter.cpp
- Delete runTimerTask, report_tenant_disk_usage, count_tenant
count_tenant_slog, count_tenant_clog, count_server_slog
count_server_clog, count_server_meta, count_tenant_tmp implementations
- Delete commented-out set_tenant_data_usage
- get_data_disk_used_size now
1. MTL_SWITCH to the target tenant
2. Calls count_tenant_data(tenant_id) to populate TENANT_DATA + TENANT_META_DATA
3. Fetches tmp file usage via ObTenantTmpFileManager
4. Reads the three needed types from result_map_ (same as before)

### ob_server.cpp
- Delete TG_SCHEDULE(lib::TGDefIDs::DiskUseReport, ...)
- Delete TG_START(lib::TGDefIDs::DiskUseReport)
- Delete TG_STOP(lib::TGDefIDs::DiskUseReport)
- Delete TG_WAIT(lib::TGDefIDs::DiskUseReport)
- Delete TG_DESTROY(lib::TGDefIDs::DiskUseReport)

## Verification
- Built and deployed to test instance (port 2915)
- SELECT * FROM __all_virtual_unit returned data_disk_in_use = 20480000
- Observer log confirmed: no DiskUseReport messages
- No errors or warnings related to ObDiskUsageReportTask in logs

## Scope & Limitations
- Targeted at seekdb (single-tenant, no tenant drop).
- If __all_virtual_unit is queried heavily, the on-demand computation cost
will be incurred per query. This is acceptable because
(a) the virtual table is typically low-QPS monitoring, and
(b) single-tenant tablet count is bounded.
- The result_map_ is still allocated at init and serves as the temp
buffer during on-demand computation.

## Related flame graph
http://obperf.oceanbase-dev.com/files/profile_20260520115549__root.flame.svg
The periodic ObCTASCleanUpTask::runTimerTask spends 100% of its CPU
time in schema fetching: get_table_schema triggers full SQL-based
schema fetch (fetch_table_schema via ObSchemaServiceSQLImpl) for every
table in the tenant. Flame graph analysis shows this accounts for ~0.12%
of total CPU samples (106/85128), all consumed by the schema fetch path.

Since seekdb is single-node, two checks are unnecessary and were removed
- create_host_str comparison: always matches on single node
- TEMP_TAB_PROXY_RULE branch: OBProxy never exists in single-node mode
is_obproxy_create_tmp_tab always returns false

After removing these, all remaining schema fields used by the cleanup
logic (session_id, table_type, schema_version, database_id, table_name
name_case_mode, is_tmp_table, is_in_recyclebin) are available in
ObSimpleTableSchemaV2. The per-table get_table_schema call is replaced
with get_simple_table_schema, which reads directly from ObSchemaMgr
in-memory hash table without any SQL query.

This eliminates the per-table SQL schema fetch overhead entirely
reducing the timer task's CPU consumption from ~106 samples to near zero.
Seekdb runs only a single sys tenant, so the Linux tc-style multi-tenant
WRR scheduling tree and token-bucket rate limiter are entirely dead weight
(flamegraph showed 1.31% CPU in QDiscRoot::do_thread_work).

## What changed

### Core: IO path bypasses TC queues
- `ObTenantIOSchedulerV2::schedule_request` now submits IO directly to
the device channel: prepare → get_device_channel → device_channel->submit
- Previously: TC queue inc_ref → QSchedCallback::handle → dec_ref, with
WRR scheduling and token-bucket checks in between

### Deleted: entire TC library (deps/oblib/src/lib/tc/)
- 40 files, ~2900 lines: ob_tc.cpp/h, ob_tc_interface.cpp, ob_tc_limit.cpp
ob_tc_stat.cpp, ob_tc_wrapper.cpp, deps/*, test/*, Makefile, README
- All qdisc_*/tclimit_*/TCRequest references removed from the codebase

### Removed: ObIOManagerV2 and its lifecycle
- ObIOManager::init/start/stop/destroy no longer call OB_IO_MANAGER_V2
- QSchedCallback (ITCHandler subclass) deleted

### Cleaned: ObTrafficControl and ObSharedDeviceControlV2
- Removed register_bucket, add_shared_device_limits, limit_ids_
- Removed qdisc_add_limit/qdisc_set_limit calls from add_group/inner_calc_
- Simplified add_group signature (no longer needs qid/limit_ids params)
- Removed TCRequest qsched_req_ member from ObIORequest
- Removed #include "lib/tc/ob_tc.h" from ob_io_define.h

### Preserved: tenant IO group tracking (independent of TC)
- group_id_index_map_ / get_group_index / io_usage_ are NOT removed
- They provide per-group IO statistics and have no code dependency on TC

## Verification
- Compile: make -j80 in build_release, no link errors
- Deploy: seekdb starts and accepts connections
- Sysbench: read/write workload runs without errors
- 47 files changed, +47 / -3681 lines
… mode

Eliminate thread_local ObTenantBase and all tenant-switching overhead by
migrating to a single global ObTenantBase pointer (g_tenant_ptr). MTL
reads directly from the real ObTenant heap object — no copies, no dual
objects.

- ob_tenant_base.h: Replace thread_local with inline globals g_tenant_ptr
and g_tenant_ctx. Both get_tenant and get_tenant_local return
g_tenant_ptr — a single unified pointer.
- ob_tenant_base.cpp: set_tenant and all ObTenantSwitchGuard methods
gutted to no-ops. switch_to(uint64_t, bool) retains a zero-cost
readiness check: g_tenant_ptr != &g_tenant_ctx, returning
OB_TENANT_NOT_IN_SERVER until the real tenant is available. This
allows MTL_SWITCH to safely skip its body during early init.
- ob_define.h: ob_get_tenant_id returns constant OB_SYS_TENANT_ID.

- ob_tenant.cpp: g_tenant_ptr = this set after create_mtl_module and
before init_mtl_module. All operator= copies removed — MTL reads
the real ObTenant directly via the global pointer.
- ob_table_service_client.cpp: init_tenant_env gutted to no-op.
- ob_dynamic_thread_pool.h: ObResetThreadTenantIdGuard gutted to no-op.
- ob_all_virtual_thread.cpp: &ob_get_tenant_id → OB_SYS_TENANT_ID.

g_tenant_ptr starts as &g_tenant_ctx (a dummy, no MTL services). After
create_mtl_module registers all MTL services on the real ObTenant
g_tenant_ptr = this. MTL_SWITCH's switch_to checks g_tenant_ptr !=
&g_tenant_ctx — a single pointer compare — to gate MTL access until
the tenant is ready. Background threads that arrive early skip safely.

- Deploy: seekdb instance starts and accepts connections
- Sysbench: oltp_point_select 4-thread 30s (702K txns, 0 errors)
oltp_read_write 4-thread 30s (14K txns, 2 deadlock retries)
- obtest: t/stanby/basic.test passes (3/3: switchover, switchback, failover)
…k_in_set

Problem
Obperf flame graph shows find_task_in_set consumes 1680 samples (1.41%)
but the binary search comparator (CompareForSet) only compares timer_ and
task_ pointers. The 1150-sample (0.97%) TaskToken temporary object
construction — typeid(*task).name + strncpy / strnlen — is entirely
wasted work. This overhead is doubled because has_running_task calls
find_task_in_set twice (running_task_set_ + uncanceled_task_set_) for
every pop_task event.

Fix
1. Replace compare_for_set free function with transparent functor
CompareForSet (is_transparent = void, C++14 heterogeneous lookup).
Add (TaskToken*, pair) and (pair, TaskToken*) overloads so
std::lower_bound can compare directly against {timer, task} pointers.
2. In find_task_in_set, pass std::pair<const ObTimer*, const ObTimerTask*>
as the search key instead of constructing a temporary TaskToken.
3. All existing call sites (insert_unique, find) remain compatible via
the (TaskToken*, TaskToken*) overload.

Expected benefit: ~1150 samples (0.97% total CPU) eliminated from
find_task_in_set, reducing it from 1.41% to ~0.45%.
…cture

## Motivation

sys_hook_impl/SYS_HOOK was originally designed to wrap blocking syscalls with
WaitGuard, which set Thread::blocking_ts_ and Thread::wait_event_ so that
__all_virtual_thread could display STATUS/WAIT_EVENT/LOOP_TS. This supported
dynamic th_worker scaling by tracking when workers were blocked.

The th_worker scaling mechanism has since been completely refactored. The hook
layer, WaitGuard/JoinGuard/RpcGuard classes, and the 6 TLS variables they
maintained became dead code with no remaining consumers.

## What was removed

### Thread class (thread.h/thread.cpp)
- 4 Guard classes: BaseWaitGuard, WaitGuard, JoinGuard, RpcGuard
- 5 WAIT_* constants: WAIT, WAIT_IN_TENANT_QUEUE, WAIT_FOR_IO_EVENT
WAIT_FOR_LOCAL_RETRY, WAIT_FOR_PX_MSG
- 6 TLS variables: wait_event_, blocking_ts_, loop_ts_, rpc_dest_addr_
pcode_, thread_joined_
- update_loop_ts simplified to just clear_lock

### ob_tenant_hook.cpp
- Removed: sys_hook_impl, SYS_HOOK macro, in_sys_hook, ob_pthread_cond_wait
ob_pthread_cond_timedwait, Linux futex_hook
- Linux ob_epoll_wait: simplified to BKGDSessInActiveGuard + direct epoll_wait
- ob_pthread_cond_timedwait_us: kept, Linux path now calls pthread_cond_timedwait directly

### Hook function callers
- ob_pthread_cond_wait -> pthread_cond_wait (3 call sites)
- ob_pthread_cond_timedwait -> pthread_cond_timedwait (2 call sites)
- Removed extern declarations from ob_define.h

### futex layer
- ob_futex.h: Linux now uses inline syscall(SYS_futex, ...) directly instead of
futex_hook routing. Win/macOS keep futex_hook forwarding for platform emulation.
- ob_futex.cpp: removed Linux weak symbol (redundant, strong symbol removed from
ob_tenant_hook.cpp)
- win32_unwind_stubs.c: removed ob_epoll_wait and futex_hook stubs

### ~26 WaitGuard call sites
Deleted WaitGuard guard(...) constructions in 14 files without replacement
ob_lock_memtable.cpp (5), ob_table_lock_service.cpp (7)
ob_signal_handle.cpp, ob_tenant.cpp, ob_storage_rpc.cpp
ob_local_device.cpp, obmp_query.cpp, obmp_stmt_execute.cpp
obmp_stmt_prexecute.cpp, ob_data_access_service.cpp
ob_lob_handler.cpp, ob_io_struct.cpp, ob_dtl_basic_channel.cpp
ob_px_sqc_async_proxy.cpp, ob_rpc_proxy.ipp

### Virtual table columns
- __all_virtual_thread: removed STATUS, WAIT_EVENT, LOOP_TS columns.
Remaining: tid, tname, latch_wait, latch_hold, trace_id, cgroup_path, numa_node (7 cols)
- Regenerated ob_inner_table_schema.*.cpp via generate_inner_table_schema.py
- Removed STATUS from GV$OB_THREAD view SELECT

### ObThWorker
- Removed blocking_ts accessor and blocking_ts_ member

### lua diagnostic API (ob_lua_api.cpp)
- Removed loop_ts, blocking_ts, join_addr, sleep_us, rpc_dest_addr, pcode, event reads
- Simplified STATUS to "Run"/"Sleep" based on sleep_us_ only
- Simplified WAIT_EVENT to latch wait info only

## What was preserved
- Windows/macOS ob_epoll_wait (pure platform emulation, no hook)
- Windows futex_hook (WaitOnAddress/WakeByAddressSingle emulation, WaitGuard line removed)
- macOS futex_hook (__ulock_wait/__ulock_wake emulation, WaitGuard line removed)
- ob_pthread_cond_timedwait_us (cross-platform, with platform emulation paths)

## Verification
- Build: clean compile of seekdb
- Virtual table: SELECT * FROM __all_virtual_thread returns 7 columns correctly
- Sysbench: oltp_point_select 30s run, ~34K TPS, 0 errors, p99 latency 0.99ms
…ond+pending bitmask

ObMemoryDump used an 8-slot ObLightyQueue + pre-allocated task pool + mutex
bitmask (~100 lines) to serialize multi-producer requests. Signal 62 runs in
a dedicated SignalHandle thread via sigtimedwait, not a raw signal handler
so all producers are ordinary thread contexts that can safely use mutex+cond.
The queue/pool machinery is unnecessary.

Replace queue+pool with a standard mutex+cond pattern
Producer (RPC/Signal/stop) Consumer (MemoryDump thread)
──────── ────────
lock → set pending bit lock → if pending_==0: wait(10s)
→ signal → unlock snapshot pending_, copy task, clear
unlock
DUMP(b1) → handle DUMP task
STAT_LABEL(b0) → handle STAT_LABEL

Key decisions
- pending_ is a plain int (bitmask: STAT_LABEL=1, DUMP=2), protected by mutex
- DUMP has priority over STAT_LABEL to prevent debug-DUMP from being starved
- cond_.wait(10s) serves dual purpose: event-driven wakeup AND timer heartbeat
for periodic STAT_LABEL (every ~10s)
- stop signals cond to avoid shutdown delay with large timeout
- Signal 62 producer uses a stack-allocated task instead of pool allocation

- deps/oblib/src/lib/alloc/memory_dump.h
Removed: ObLightyQueue, task_mutex_, tasks_[8], avaliable_task_set_, TASK_NUM
push, alloc_task, free_task
Added: ObThreadCond cond_, int pending_, ObMemoryDumpTask pending_dump_task_
PENDING_STAT_LABEL=1, PENDING_DUMP=2, request_dump

- deps/oblib/src/lib/alloc/memory_dump.cpp
init: cond_.init replaces queue_.init
stop: cond_.signal before TG_STOP for immediate wakeup
destroy: cond_.destroy replaces queue_.destroy
Deleted: push, alloc_task, free_task
Added: request_dump — copy task under mutex, signal cond
Simplified: generate_mod_stat_task — lock→set bit→signal→unlock
Rewritten: run1 — ObThreadCondGuard RAII, snapshot+clear pending_ under
lock, DUMP-first priority, timer-based STAT_LABEL heartbeat
Fixed: handle no longer calls free_task (task is stack variable)

- src/observer/ob_dump_task_generator.cpp
Stack-allocated task replaces alloc_task/free_task/push
ObMemoryDumpTask task; → fill fields → mem_dump.request_dump(task)

| Path | Method | Result |
|---------------|------------------------------------|--------|
| RPC | ALTER SYSTEM REFRESH MEMORY STAT | Pass |
| Signal 62 | kill -62 + etc/dump.config | Pass |
| Timer | 10s heartbeat via cond_.wait | Pass |
| Stop | kill <pid> (0.1s shutdown) | Pass |

3 files, +87/-108 lines
…PU overhead

When the DDL task queue is empty (no DDL activity), the scheduler thread
do_work idles with wakeup_cnt_-based cond_wait. The max idle time was
1s, causing the thread to wake every second for a no-op check even when
no DDL tasks exist — consuming CPU cycles repeatedly in the flame graph.

Two constants were changed
ob_ddl_scheduler.h DDLIdling::get_idle_interval_us: 1s → 30s
ob_ddl_task.h MAX_IDLE_TIME_US: 1s → 30s

Wakeup safety (no signal loss / no processing delay)
- inner_schedule_ddl_task always calls idler_.wakeup after push_task
which increments wakeup_cnt_ and broadcasts the cond var.
- pthread_cond_wait atomically releases mutex + sleeps, so wakeup
called between check and wait is not lost (wakeup_cnt_ acts as a
persistent signal, not a transient edge).
- Therefore a newly-scheduled DDL task interrupts the idle immediately
regardless of max idle interval. The 30s is only the upper bound when
genuinely idle.

Tested on seekdb single-node instance
- Observed __all_ddl_task_status count 0→1→0 during CREATE INDEX
confirming tasks flow through ObDDLScheduler::do_work.
- Task processing completed in ~1s, well below 30s — wakeup intact.
…and hash map

## Context

ObPxTargetMgr runs a 500ms periodic timer that coordinates PX worker
usage across a distributed cluster: followers RPC-sync local usage to
the leader, the leader aggregates into a per-server hash map. In
single-server single-tenant SeekDB, all of this is wasted work.

Flame graph: TimerTask::runTimerTask = 959 samples (0.81% CPU), all in
DCHash traversal and map rebuild.

## What was removed (549 lines across 4 files)

### Timer & distributed coordination (306 lines)
- TimerTask inner class, timer_task_ member, run_timer_task
- ObPxResRefreshFunctor (always-true need_refresh_all_ dead code)
- TG_START/TG_SCHEDULE/TG_STOP/TG_WAIT timer lifecycle
- refresh_statistics and its entire call chain
get_dummy_leader / check_dummy_location_credible / get_role /
refresh_dummy_location / query_statistics (RPC, 62 lines)
- rpc_proxy_, cluster_id_, dummy_cache_leader_, debug flags
- PX_REFRESH_TARGET_INTERVEL_US / PX_REFRESH_CHECK_ALIVE_INTERVAL_US
- Role init: FOLLOWER -> LEADER

### Data structure (243 lines)
- ServerTargetUsage struct (peer/local/report, 38 lines) and its
OB_SERIALIZE_MEMBER — 3-field distributed accounting protocol
- hash::ObHashMap<ObAddr, ServerTargetUsage> global_target_usage_
always held exactly 1 entry (self); replaced by int64_t px_target_used_
- PX_SERVER_TARGET_BUCKET_NUM macro
- get_global_target_usage -> inline get_px_target_used
- ObPxGlobalResGather::operator (dead code after map removal)

### Simplified methods
- apply_target: sums worker_map, single compare, SpinWLockGuard
- release_target: single subtraction, no per-server map updates
- get_all_target_info: hardcoded single self-entry
- reset_*_statistics: px_target_used_ = 0
- update_peer_target_used: no-op stub
- gather_global_target_usage: direct push, no foreach
- init/reset: removed hash map create/clear

## What was preserved

- apply_target / release_target — PX admission control gates
concurrent queries against SET GLOBAL parallel_servers_target
- reset_leader_statistics / update_peer_target_used — RPC handler
stubs kept for compilation; multi-server restore points
- All ObPxTargetMgr public API unchanged

## Verification (SeekDB port 12881, debug build)

- Instance boots, PX queries execute normally
- __all_virtual_px_target_monitor shows is_leader=1, correct target
- SET GLOBAL parallel_servers_target = N syncs to monitor correctly
- Zero PX timer activity in logs, zero PX errors

## How to restore multi-server PX coordination

1. Re-add TimerTask + TG_SCHEDULE in ob_px_target_mgr
2. Re-add refresh_statistics + role-detection call chain
3. Re-add rpc_proxy_ / cluster_id_ / dummy_cache_leader_
4. Re-add ServerTargetUsage + global_target_usage_ hash map
5. Change init role back to FOLLOWER
RPC handlers and ObPxTargetMgr API are ready.

(cherry picked from commit ffc4f71b2386)
Problem
-------
Flame graph analysis on an idle instance shows ObLockWaitMgr::run1
consumes 2.11% of total CPU, with 51.6% of that spent inside
check_timeout scanning 16384 hash buckets every 100ms -- even when
no transactions are waiting for locks.

Root Cause
----------
FixedHash2::quick_next traverses ALL bucket headers regardless of
whether they contain data. On an empty hash with LOCK_BUCKET_COUNT=16384
each check_timeout call does 16384 iterations of atomic loads and
pointer checks before returning NULL. At 10 calls/second (100ms sleep)
this wastes ~1% CPU on a completely idle system.

The while-loop condition `!has_set_stop || !is_hash_empty` always
evaluates to true during normal operation (has_set_stop is false)
so is_hash_empty is never even reached to short-circuit the loop.

Fix
---
Two changes, both using the existing total_wait_node_ counter
1. is_hash_empty: Replace O(16384) hash scan with O(1) atomic read
of total_wait_node_. This counter is already maintained correctly
- INC in wait before hash_.insert
- DEC in wait on wait failure, in fetch_waiter after hash_.del
and in retire_node after hash_.del
The INC-before-insert / DEC-after-delete ordering guarantees the
counter never reads zero when nodes are present in the hash.

2. run1: Guard check_timeout with total_wait_node_ > 0 check.
When no nodes are waiting, skip the entire hash scan, QSync
CriticalGuard acquire/release, and fetch_deadlocked_sessions_ call.

Performance Impact
------------------
Idle instance: run1 CPU drops from ~2.1% to ~0.5% (only usleep +
periodic mapper dump + WaitEvent tracking remain).

Instance with waiting nodes: No change. check_timeout executes
normally with the same hash scan behavior as before.

Safety
------
total_wait_node_ is an existing int64_t atomically maintained across
all insert/delete paths. The counter covers
- Normal wait → wakeup lifecycle
- wait failure (has_set_stop / wakeup already happened)
- Timeout / killed session / deadlock cleanup via retire_node
- transform_row_lock_to_tx_lock re-insert path
Problem (from flame graph analysis)
update_upper_trans_version_and_gc_sstable runs every 30s to scan all
tablets and clean up old minor SSTables. The flame graph shows ~35
samples spent in check_tablet_status_for_read_all_committed — a
4-layer MDS spinlock path called per-tablet during tablet handle
acquisition. Additionally, check_ls_migration_status was called
inside the per-tablet loop, doing redundant LS-level revalidation
for every tablet.

Changes
1. Configurable tablet read mode in ObCompactionScheduleIterator
- Added tablet_get_mode_ member (default: READ_ALL_COMMITED
backward compatible for merge scheduling path)
- Added set_tablet_get_mode setter
- get_tablet_handle now uses the member instead of hardcoded
READ_ALL_COMMITED

2. GC iterator uses READ_WITHOUT_CHECK mode
- After build_iter in update_upper_trans_version_and_gc_sstable
set mode to READ_WITHOUT_CHECK
- Skips check_tablet_status_for_read_all_committed and its 4-layer
MDS spinlock (MdsUnit + MdsRow), since GC only reads tablet table
store metadata and does not traverse MDS transaction state

3. Hoist LS migration check out of per-tablet loop
- check_ls_migration_status moved from inside the while loop to
before it, so it runs once per LS instead of once per tablet
- If the LS is migrating, skip all tablets in that LS

Safety analysis
- READ_WITHOUT_CHECK is safe for GC: the GC task only reads SSTable
metadata from the tablet table store; it does not depend on MDS
transaction consistency. The worst case is reading a stale table
store snapshot, which means a GC opportunity is missed for one
round — the next 30s iteration will catch it.
- LS migration check hoisting is safe: migration status changes at
LS granularity, not per-tablet. Once an LS enters migration, no
tablet within that LS should be processed. Processing tablets
under a migrating LS was already a no-op (the old code fell
through to the empty else-if branch for ls_is_migration).

Testing
- Compiled and deployed to running seekdb lite instance
- Verified GC task runs normally (no WARN/ERROR in observer.log)
- Verified SSTable cleanup after table drop (dropped tables'
SSTables were removed within one GC cycle)
- Note: full major merge (merge → GC old minor SSTables) not tested
on seekdb lite (compaction_scn_not_update issue); needs validation
on a full OceanBase cluster

Follow-up
- Verify major merge + GC path on full cluster
- Measure CPU reduction in flame graph after deployment
…ObVector with token-resident design

## Motivation
ObSortedVector's insert_unique and remove operations trigger O(n) memmove
which showed as ~1.66% CPU (wcscat/memmove) in flame graphs under load.
The sorted invariant is only needed for O(1) front-peek in pop_task; an
unsorted vector with full scan eliminates the memmove cost entirely.

## Design: Token-Resident Queue
Instead of removing tokens from priority_task_queue_ on dispatch and
re-inserting them on repeat reschedule, tokens stay in the queue for their
entire lifetime. Dispatch state is encoded as
scheduled_time_ = TOKEN_DISPATCHED (INT64_MAX)

### Key behavioral changes

1. schedule_task (new task)
- push_back + notify (was insert_unique + conditional notify)
- Always notifies because there is no sorted order to optimize against

2. schedule_task (repeat task after runTimerTask completion)
- Updates scheduled_time_ in-place, token stays in queue
- notify_all unconditional (was insert_unique + conditional notify on head)

3. schedule_task (non-repeat task after completion)
- Scans queue to find token, swap-with-last + pop_back (O(1) removal)
- Then delete_token

4. pop_task (scan loop in run1)
- Full O(n) scan every tick instead of O(1) front-peek
- Skips TOKEN_DISPATCHED tokens
- Handles self-conflict: if running_task_token == token, skips silently
- On dispatch: sets scheduled_time_ = TOKEN_DISPATCHED, token stays in queue

5. cancel_task
- swap-with-last + pop_back instead of ObSortedVector::remove
- Skips delete_token for TOKEN_DISPATCHED tokens (they're in running_set_)

6. stop
- STEP2 skips dispatched tokens to avoid double-delete with STEP3

### Removed
- min_scheduled_time_: eliminated to avoid maintenance burden across 3
mutation paths (cancel, schedule new, schedule repeat)
- compare_for_queue (free function): replaced by the iterating scan
- check_clock and CLOCK_SKEW_DELTA/CLOCK_ERROR_DELTA: per-tick clock
sanity check removed to reduce scheduling path overhead
- ObDIActionGuard / ObBKGDSessInActiveGuard: removed from scheduling hot path
- Temporary TaskToken construction in find_task_in_set
CompareForSet now supports std::pair<ObTimer*, ObTimerTask*> as
transparent lookup key (C++14 is_transparent)

### Trade-off
At idle, pop_task's O(n) full scan is slightly more expensive than the
original O(1) front-peek (~0.25% vs ~0.09% self samples). The benefit
manifests under load: zero memmove on insert/remove, zero temporary
allocations for set lookups, and no sorted-order maintenance.
Problem
KVCache wash timer task (KVStoreWashTask) runs every ~800ms and consumes
~20% CPU when the system is idle. The hotspot is `clean_garbage_node`
which traverses 200K hash bucket slots per call. Root cause
`ObKVCacheStore::wash` unconditionally returns `true`, so
`ObKVGlobalCache::wash` always calls `map_.clean_garbage_node` even
when no memory was reclaimed.

Change
`ObKVCacheStore::wash` returns `reclaimed_size > 0` instead of `true`.
When wash reclaims nothing (idle), `clean_garbage_node` is skipped
entirely. When cache pressure exists, wash reclaims memory and
`clean_garbage_node` runs normally to clean garbage hash nodes.

Verification (A/B test on 8C 61GB, remote deploy + local sysbench)
- obperf: clean_garbage_node 20.0% -> 0.0%, wash overhead 20.6% -> 0.9%
- sysbench point_select: QPS unchanged (113K vs 114K, within noise)
- Cache pressure: wash reclaims 2-35MB/cycle, memory comparable
…n cond_timedwait

ObCSFetcher previously consumed ~4% idle CPU by usleep(10ms)-polling
schema version in IDLE mode (no async vector index tables). Replace this
with a cond_timedwait(10s) that wakes immediately on schema publish.

Notification chain
publish_schema → ObTenantSwitchGuard(tenant_id) → MTL_CTX
→ ObTenantBase::on_schema_publish [virtual]
→ ObTenant::on_schema_publish [override]
→ ObChangeStreamMgr::get_fetcher.notify_schema_changed
→ idle_cond_.signal

Design decision: use a virtual function on ObTenantBase (overridden by
ObTenant) instead of callback arrays. This keeps publish_schema unaware
of business modules, ensures zero runtime overhead, and makes the call
chain trivially traceable. Additional modules that need schema publish
notification simply add their code to ObTenant::on_schema_publish.

Double-checked locking in IDLE branch prevents TOCTOU races: re-read
schema version under the cond guard before entering cond_timedwait.
stop signals idle_cond_ for fast shutdown.
## Background
Flame graph analysis (profile_20260520225116, obperf) on ObSimpleThreadPoolDynamicMgr::run1
shows the manager thread wakes up every 200ms to iterate all registered simple thread pools
and call reap_workers. The overhead breakdown
run1 total: 0.04% CPU
├─ Self (SpinRLock + loop + usleep): 59%
└─ reap_workers callees: 41%
├─ ObLatchMutex::lock (workers_lock_): 21%
├─ Threads::wait: 21%
├─ ObTimeUtility::current_time: 6%
├─ usleep: 4%
└─ Threads::destroy: 2%

## Change
- ObSimpleThreadPoolDynamicMgr::CHECK_INTERVAL_US: 200ms -> 3s
- File: deps/oblib/src/lib/thread/ob_dynamic_thread_pool.h:129

## Rationale
reap_workers only needs to clean up stopped workers. Worker lifecycle
(thread exit) is on the order of seconds, not milliseconds. A 3s interval is
sufficient to reclaim stopped workers without meaningful delay, while reducing
the wake-up frequency by 15x (200ms -> 3s), cutting CPU overhead from ~0.04%
to ~0.003%.

## Trade-off
Stopped worker threads may linger up to 3s before being reaped. This is
acceptable because worker stop events are rare (only during pool shrink) and
the memory held by a stopped worker (thread stack + small control struct) is
negligible.
…cture

## Motivation

sys_hook_impl/SYS_HOOK was originally designed to wrap blocking syscalls with
WaitGuard, which set Thread::blocking_ts_ and Thread::wait_event_ so that
__all_virtual_thread could display STATUS/WAIT_EVENT/LOOP_TS. This supported
dynamic th_worker scaling by tracking when workers were blocked.

The th_worker scaling mechanism has since been completely refactored. The hook
layer, WaitGuard/JoinGuard/RpcGuard classes, and the 6 TLS variables they
maintained became dead code with no remaining consumers.

## What was removed

### Thread class (thread.h/thread.cpp)
- 4 Guard classes: BaseWaitGuard, WaitGuard, JoinGuard, RpcGuard
- 5 WAIT_* constants: WAIT, WAIT_IN_TENANT_QUEUE, WAIT_FOR_IO_EVENT
WAIT_FOR_LOCAL_RETRY, WAIT_FOR_PX_MSG
- 6 TLS variables: wait_event_, blocking_ts_, loop_ts_, rpc_dest_addr_
pcode_, thread_joined_
- update_loop_ts simplified to just clear_lock

### ob_tenant_hook.cpp
- Removed: sys_hook_impl, SYS_HOOK macro, in_sys_hook, ob_pthread_cond_wait
ob_pthread_cond_timedwait, Linux futex_hook
- Linux ob_epoll_wait: simplified to BKGDSessInActiveGuard + direct epoll_wait
- ob_pthread_cond_timedwait_us: kept, Linux path now calls pthread_cond_timedwait directly

### Hook function callers
- ob_pthread_cond_wait -> pthread_cond_wait (3 call sites)
- ob_pthread_cond_timedwait -> pthread_cond_timedwait (2 call sites)
- Removed extern declarations from ob_define.h

### futex layer
- ob_futex.h: Linux now uses inline syscall(SYS_futex, ...) directly instead of
futex_hook routing. Win/macOS keep futex_hook forwarding for platform emulation.
- ob_futex.cpp: removed Linux weak symbol (redundant, strong symbol removed from
ob_tenant_hook.cpp)
- win32_unwind_stubs.c: removed ob_epoll_wait and futex_hook stubs

### ~26 WaitGuard call sites
Deleted WaitGuard guard(...) constructions in 14 files without replacement
ob_lock_memtable.cpp (5), ob_table_lock_service.cpp (7)
ob_signal_handle.cpp, ob_tenant.cpp, ob_storage_rpc.cpp
ob_local_device.cpp, obmp_query.cpp, obmp_stmt_execute.cpp
obmp_stmt_prexecute.cpp, ob_data_access_service.cpp
ob_lob_handler.cpp, ob_io_struct.cpp, ob_dtl_basic_channel.cpp
ob_px_sqc_async_proxy.cpp, ob_rpc_proxy.ipp

### Virtual table columns
- __all_virtual_thread: removed STATUS, WAIT_EVENT, LOOP_TS columns.
Remaining: tid, tname, latch_wait, latch_hold, trace_id, cgroup_path, numa_node (7 cols)
- Regenerated ob_inner_table_schema.*.cpp via generate_inner_table_schema.py
- Removed STATUS from GV$OB_THREAD view SELECT

### ObThWorker
- Removed blocking_ts accessor and blocking_ts_ member

### lua diagnostic API (ob_lua_api.cpp)
- Removed loop_ts, blocking_ts, join_addr, sleep_us, rpc_dest_addr, pcode, event reads
- Simplified STATUS to "Run"/"Sleep" based on sleep_us_ only
- Simplified WAIT_EVENT to latch wait info only

## What was preserved
- Windows/macOS ob_epoll_wait (pure platform emulation, no hook)
- Windows futex_hook (WaitOnAddress/WakeByAddressSingle emulation, WaitGuard line removed)
- macOS futex_hook (__ulock_wait/__ulock_wake emulation, WaitGuard line removed)
- ob_pthread_cond_timedwait_us (cross-platform, with platform emulation paths)

## Verification
- Build: clean compile of seekdb
- Virtual table: SELECT * FROM __all_virtual_thread returns 7 columns correctly
- Sysbench: oltp_point_select 30s run, ~34K TPS, 0 errors, p99 latency 0.99ms
hnwyllmm and others added 28 commits July 30, 2026 15:48
* Derive stable UUID from machine ID and base directory

* fix comments

* derive stable UUID from container identity

---------

Co-authored-by: LINxiansheng <LINxiansheng@users.noreply.github.com>
* fix(mysqltest): wait for target major merge round

* Merge remote-tracking branch 'origin/master' into issue/2026072400117720278

* Merge origin/master into issue/2026072400117720278

* Merge branch 'master' of into issue/2026072400117720278

* fix(mysqltest): stabilize macOS GIS and timing cases

* test(mysqltest): restore GIS plan assertions

* test(mysqltest): revert compaction wait changes

---------

Co-authored-by: hnwyllmm <hnwyllmm@users.noreply.github.com>
Co-authored-by: ep-12221 <ep-12221@users.noreply.github.com>
* Remove idle caching from inner SQL connection pool

* Move inner SQL client tracking from connections to sessions

* Move inner SQL connection lifecycle out of connection pool

* Bypass connection pool for session-bound inner SQL connections

* Remove inner SQL connection pools and inject lifecycle callbacks

* Remove obsolete per-client inner SQL inactivity handling

* Replace inner SQL lifecycle callbacks with weak symbols

* Fix build regressions after inner SQL pool removal

* Remove obsolete inner SQL connection and session pooling logic

* Manage inner SQL connections with shared guards

* Remove obsolete observer statistics and session pool remnants

---------

Co-authored-by: hnwyllmm <hnwyllmm@users.noreply.github.com>
Co-authored-by: ep-12221 <ep-12221@users.noreply.github.com>
Co-authored-by: footka <footka@users.noreply.github.com>
* Reduce comparison evaluator template instantiations

* Reduce datum batch hash template instantiations

* Reduce fixed-double batch hash template instantiations

* Reduce decimal-int cast template instantiations

* Reduce object comparison operator template instantiations

* Reduce decimal-int comparison template instantiations

* Reduce fixed-double comparison template instantiations

* Reduce decimal-int relation evaluator template instantiations

* Reduce type-class comparison evaluator template instantiations

* Reduce decimal-int batch scale template instantiations

---------

Co-authored-by: LINxiansheng <LINxiansheng@users.noreply.github.com>
Co-authored-by: footka <footka@users.noreply.github.com>
Make DDL stats sync fail fast on stats row lock conflicts, then let the existing DDL retry path keep TAKE_EFFECT with a one-shot retry delay. Add a mysqltest reproducing concurrent gather_schema_stats and column redefinition.
Use ob_fast_localtime in ObTime2Str to avoid localtime_r/strftime timezone hot paths under verbose logging. Also fix ob_fast_localtime zero-second cache initialization and remove duplicate DTL buffer free counting.

Co-authored-by: footka <footka@users.noreply.github.com>
…se#1242)

Co-authored-by: LINxiansheng <LINxiansheng@users.noreply.github.com>
* remove case

* Remove obsolete virtual thread diagnostic table

---------

Co-authored-by: ep-12221 <ep-12221@users.noreply.github.com>
* fix: stabilize macOS mysqltest cases

* test: stabilize GIS cases across platforms

---------

Co-authored-by: ep-12221 <ep-12221@users.noreply.github.com>
Co-authored-by: hnwyllmm <hnwyllmm@users.noreply.github.com>
Co-authored-by: hnwyllmm <hnwyllmm@users.noreply.github.com>
Co-authored-by: ep-12221 <ep-12221@users.noreply.github.com>
@hnwyllmm

hnwyllmm commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

The mapping Dima issue is related to inner SQL optimization.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants