| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "benchmark.h" |
| 4 | |
| 5 | #define MAX_THREADS 64 |
| 6 | #define TEST_DURATION_SEC 1 |
| 7 | #define STOP_SIGNAL UINT64_MAX |
| 8 | #define NUM_LOCK_TYPES 5 |
| 9 | |
| 10 | // Structure to store summary stats |
| 11 | typedef struct { |
| 12 | double locks_per_sec[NUM_LOCK_TYPES][7]; // [lock_type][thread_count_index] |
| 13 | } summary_stats_t; |
| 14 | |
| 15 | typedef struct { |
| 16 | uint64_t locks; |
| 17 | usec_t test_time; |
| 18 | volatile int ready; |
| 19 | } thread_stats_t; |
| 20 | |
| 21 | typedef struct { |
| 22 | netdata_cond_t cond; // Individual condition for each thread |
| 23 | netdata_mutex_t cond_mutex; // Individual mutex for each thread |
| 24 | uint64_t run_flag; // Individual run flag for each thread |
| 25 | } thread_control_t; |
| 26 | |
| 27 | typedef struct { |
| 28 | uint64_t protected_counter; |
| 29 | thread_stats_t stats[MAX_THREADS]; |
| 30 | thread_control_t thread_controls[MAX_THREADS]; // Array of per-thread controls |
| 31 | } lock_control_t; |
| 32 | |
| 33 | typedef enum { |
| 34 | LOCK_MUTEX, |
| 35 | LOCK_RWLOCK, |
| 36 | LOCK_SPINLOCK, |
| 37 | LOCK_RW_SPINLOCK, |
| 38 | LOCK_WAITQ |
| 39 | } lock_type_t; |
| 40 | |
| 41 | typedef struct { |
| 42 | int thread_id; |
| 43 | lock_type_t type; |
| 44 | WAITQ_PRIORITY priority; // For waitq only |
| 45 | lock_control_t *control; |
| 46 | void *lock; // Points to the actual lock |
| 47 | ND_THREAD *thread; |
| 48 | } thread_context_t; |
| 49 | |
| 50 | static const char *lock_names[] = { |
| 51 | "Mutex", |
| 52 | "RWLock", |
| 53 | "Spinlock", |
| 54 | "RW Spinlock", |
| 55 | "WaitQ" |
| 56 | }; |
| 57 | |
| 58 | static const char *priority_to_string(WAITQ_PRIORITY p) { |
| 59 | switch(p) { |
| 60 | case WAITQ_PRIO_URGENT: return "URGENT"; |
| 61 | case WAITQ_PRIO_HIGH: return "HIGH"; |
| 62 | case WAITQ_PRIO_NORMAL: return "NORMAL"; |
| 63 | case WAITQ_PRIO_LOW: return "LOW"; |
| 64 | default: return "UNKNOWN"; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | static void print_summary(const summary_stats_t *summary) { |
| 69 | fprintf(stderr, "\n=== Performance Summary (Million locks/sec) ===\n\n"); |
| 70 | fprintf(stderr, "%-12s %8s %8s %8s %8s %8s %8s %8s\n", |
| 71 | "Lock Type", "1", "2", "4", "8", "16", "32", "64"); |
| 72 | fprintf(stderr, "------------------------------------------------------------------------------\n"); |
| 73 | |
| 74 | for(int type = 0; type < NUM_LOCK_TYPES; type++) { |
| 75 | fprintf(stderr, "%-12s", lock_names[type]); |
| 76 | for(int i = 0; i < 7; i++) { // 6 different thread counts |
| 77 | fprintf(stderr, " %8.2f", summary->locks_per_sec[type][i] / 1000000.0); |
| 78 | } |
| 79 | fprintf(stderr, "\n"); |
| 80 | } |
| 81 | fprintf(stderr, "\n"); |
| 82 | } |
| 83 | |
| 84 | static void wait_for_signal(netdata_cond_t *cond, netdata_mutex_t *mutex, uint64_t *flag) { |
| 85 | netdata_mutex_lock(mutex); |
| 86 | while (*flag == 0) |
| 87 | netdata_cond_wait(cond, mutex); |
| 88 | netdata_mutex_unlock(mutex); |
| 89 | } |
| 90 | |
| 91 | static void benchmark_thread(void *arg) { |
| 92 | thread_context_t *ctx = (thread_context_t *)arg; |
| 93 | thread_stats_t *stats = &ctx->control->stats[ctx->thread_id]; |
| 94 | thread_control_t *thread_control = &ctx->control->thread_controls[ctx->thread_id]; |
| 95 | |
| 96 | while(1) { |
| 97 | wait_for_signal(&thread_control->cond, &thread_control->cond_mutex, &thread_control->run_flag); |
| 98 | |
| 99 | if (thread_control->run_flag == STOP_SIGNAL) |
| 100 | break; |
| 101 | |
| 102 | usec_t start = now_monotonic_high_precision_usec(); |
| 103 | uint64_t local_counter = 0; |
| 104 | |
| 105 | switch(ctx->type) { |
| 106 | case LOCK_MUTEX: { |
| 107 | netdata_mutex_t *mutex = ctx->lock; |
| 108 | while (thread_control->run_flag) { |
| 109 | netdata_mutex_lock(mutex); |
| 110 | ctx->control->protected_counter++; |
| 111 | netdata_mutex_unlock(mutex); |
| 112 | local_counter++; |
| 113 | } |
| 114 | break; |
| 115 | } |
| 116 | |
| 117 | case LOCK_RWLOCK: { |
| 118 | netdata_rwlock_t *rwlock = ctx->lock; |
| 119 | while (thread_control->run_flag) { |
| 120 | netdata_rwlock_wrlock(rwlock); |
| 121 | ctx->control->protected_counter++; |
| 122 | netdata_rwlock_wrunlock(rwlock); |
| 123 | local_counter++; |
| 124 | } |
| 125 | break; |
| 126 | } |
| 127 | |
| 128 | case LOCK_SPINLOCK: { |
| 129 | SPINLOCK *spinlock = ctx->lock; |
| 130 | while (thread_control->run_flag) { |
| 131 | spinlock_lock(spinlock); |
| 132 | ctx->control->protected_counter++; |
| 133 | spinlock_unlock(spinlock); |
| 134 | local_counter++; |
| 135 | } |
| 136 | break; |
| 137 | } |
| 138 | |
| 139 | case LOCK_RW_SPINLOCK: { |
| 140 | RW_SPINLOCK *rw_spinlock = ctx->lock; |
| 141 | while (thread_control->run_flag) { |
| 142 | rw_spinlock_write_lock(rw_spinlock); |
| 143 | ctx->control->protected_counter++; |
| 144 | rw_spinlock_write_unlock(rw_spinlock); |
| 145 | local_counter++; |
| 146 | } |
| 147 | break; |
| 148 | } |
| 149 | |
| 150 | case LOCK_WAITQ: { |
| 151 | WAITQ *waitq = ctx->lock; |
| 152 | WAITQ_PRIORITY priority = ctx->priority; |
| 153 | while (thread_control->run_flag) { |
| 154 | waitq_acquire(waitq, priority); |
| 155 | ctx->control->protected_counter++; |
| 156 | waitq_release(waitq); |
| 157 | local_counter++; |
| 158 | } |
| 159 | break; |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | // Store results atomically |
| 164 | usec_t test_time = now_monotonic_high_precision_usec() - start; |
| 165 | __atomic_store_n(&stats->test_time, test_time, __ATOMIC_RELEASE); |
| 166 | __atomic_store_n(&stats->locks, local_counter, __ATOMIC_RELEASE); |
| 167 | __atomic_store_n(&stats->ready, 1, __ATOMIC_RELEASE); |
| 168 | } |
| 169 | } |
| 170 | |
| 171 | static void print_thread_stats(const char *test_name, int threads, thread_context_t *contexts, |
| 172 | thread_stats_t *stats, uint64_t protected_counter, |
| 173 | summary_stats_t *summary, int thread_count_idx) { |
| 174 | fprintf(stderr, "\n%-20s (threads: %d)\n", test_name, threads); |
| 175 | if (strcmp(test_name, "WaitQ") == 0) { |
| 176 | fprintf(stderr, "%4s %8s %12s %12s %12s\n", |
| 177 | "THR", "PRIO", "LOCKS", "LOCKS/SEC", "TIME (ms)"); |
| 178 | } |
| 179 | else { |
| 180 | fprintf(stderr, "%4s %12s %12s %12s\n", |
| 181 | "THR", "LOCKS", "LOCKS/SEC", "TIME (ms)"); |
| 182 | } |
| 183 | |
| 184 | uint64_t total_locks = 0; |
| 185 | double total_locks_per_sec = 0; |
| 186 | |
| 187 | for(int i = 0; i < threads; i++) { |
| 188 | uint64_t locks = __atomic_load_n(&stats[i].locks, __ATOMIC_ACQUIRE); |
| 189 | usec_t time = __atomic_load_n(&stats[i].test_time, __ATOMIC_ACQUIRE); |
| 190 | double locks_per_sec = (double)locks * USEC_PER_SEC / time; |
| 191 | total_locks_per_sec += locks_per_sec; |
| 192 | |
| 193 | if (strcmp(test_name, "WaitQ") == 0) { |
| 194 | fprintf(stderr, "%4d %8s %12"PRIu64" %12.0f %12.2f\n", |
| 195 | i, |
| 196 | priority_to_string(contexts[i].priority), |
| 197 | locks, |
| 198 | locks_per_sec, |
| 199 | (double)time / 1000.0); |
| 200 | } |
| 201 | else { |
| 202 | fprintf(stderr, "%4d %12"PRIu64" %12.0f %12.2f\n", |
| 203 | i, locks, locks_per_sec, |
| 204 | (double)time / 1000.0); |
| 205 | } |
| 206 | |
| 207 | total_locks += locks; |
| 208 | } |
| 209 | |
| 210 | if(total_locks != protected_counter) { |
| 211 | fprintf(stderr, "\nERROR: Counter mismatch!\n"); |
| 212 | fprintf(stderr, "Sum of thread counters: %"PRIu64"\n", total_locks); |
| 213 | fprintf(stderr, "Protected counter: %"PRIu64"\n", protected_counter); |
| 214 | fprintf(stderr, "Difference: %"PRIu64"\n", |
| 215 | total_locks > protected_counter ? |
| 216 | total_locks - protected_counter : |
| 217 | protected_counter - total_locks); |
| 218 | |
| 219 | fflush(stderr); |
| 220 | _exit(1); |
| 221 | } |
| 222 | |
| 223 | fprintf(stderr, "%4s %12"PRIu64"\n", "TOT", total_locks); |
| 224 | |
| 225 | // Store in summary for the final table |
| 226 | summary->locks_per_sec[contexts[0].type][thread_count_idx] = total_locks_per_sec; |
| 227 | } |
| 228 | |
| 229 | static void run_test(const char *name, int threads, thread_context_t *contexts, |
| 230 | lock_control_t *control, summary_stats_t *summary) { |
| 231 | fprintf(stderr, "\nRunning test: %s with %d threads...\n", name, threads); |
| 232 | |
| 233 | // Reset stats and counter |
| 234 | for(int i = 0; i < threads; i++) { |
| 235 | __atomic_store_n(&control->stats[i].locks, 0, __ATOMIC_RELEASE); |
| 236 | __atomic_store_n(&control->stats[i].test_time, 0, __ATOMIC_RELEASE); |
| 237 | __atomic_store_n(&control->stats[i].ready, 0, __ATOMIC_RELEASE); |
| 238 | } |
| 239 | control->protected_counter = 0; |
| 240 | |
| 241 | // Signal only the threads we need for this test |
| 242 | for(int i = 0; i < threads; i++) { |
| 243 | thread_control_t *thread_control = &control->thread_controls[i]; |
| 244 | netdata_mutex_lock(&thread_control->cond_mutex); |
| 245 | thread_control->run_flag = 1; |
| 246 | netdata_cond_signal(&thread_control->cond); |
| 247 | netdata_mutex_unlock(&thread_control->cond_mutex); |
| 248 | } |
| 249 | |
| 250 | // Wait for test duration |
| 251 | sleep_usec(TEST_DURATION_SEC * USEC_PER_SEC); |
| 252 | |
| 253 | // Signal threads to stop |
| 254 | for(int i = 0; i < threads; i++) { |
| 255 | thread_control_t *thread_control = &control->thread_controls[i]; |
| 256 | __atomic_store_n(&thread_control->run_flag, 0, __ATOMIC_RELEASE); |
| 257 | } |
| 258 | |
| 259 | // Wait for threads to report results |
| 260 | for(int i = 0; i < threads; i++) { |
| 261 | while(!__atomic_load_n(&control->stats[i].ready, __ATOMIC_ACQUIRE)) |
| 262 | sleep_usec(10); |
| 263 | } |
| 264 | |
| 265 | // Get thread count index for summary |
| 266 | int thread_count_idx; |
| 267 | switch(threads) { |
| 268 | case 1: thread_count_idx = 0; break; |
| 269 | case 2: thread_count_idx = 1; break; |
| 270 | case 4: thread_count_idx = 2; break; |
| 271 | case 8: thread_count_idx = 3; break; |
| 272 | case 16: thread_count_idx = 4; break; |
| 273 | case 32: thread_count_idx = 5; break; |
| 274 | case 64: thread_count_idx = 6; break; |
| 275 | default: thread_count_idx = 0; break; |
| 276 | } |
| 277 | |
| 278 | print_thread_stats(name, threads, contexts, control->stats, control->protected_counter, |
| 279 | summary, thread_count_idx); |
| 280 | } |
| 281 | |
| 282 | static void set_waitq_priorities(int thread_count, thread_context_t *contexts) { |
| 283 | switch(thread_count) { |
| 284 | case 1: |
| 285 | contexts[0].priority = WAITQ_PRIO_URGENT; |
| 286 | break; |
| 287 | |
| 288 | case 2: |
| 289 | contexts[0].priority = WAITQ_PRIO_URGENT; |
| 290 | contexts[1].priority = WAITQ_PRIO_HIGH; |
| 291 | break; |
| 292 | |
| 293 | case 4: |
| 294 | contexts[0].priority = WAITQ_PRIO_URGENT; |
| 295 | contexts[1].priority = WAITQ_PRIO_HIGH; |
| 296 | contexts[2].priority = WAITQ_PRIO_NORMAL; |
| 297 | contexts[3].priority = WAITQ_PRIO_LOW; |
| 298 | break; |
| 299 | |
| 300 | default: { // 8, 16, 32 |
| 301 | int threads_per_priority = thread_count / 4; |
| 302 | int remainder = thread_count % 4; |
| 303 | int thread_idx = 0; |
| 304 | |
| 305 | for (int prio = WAITQ_PRIO_URGENT; prio <= WAITQ_PRIO_LOW; prio++) { |
| 306 | int count = threads_per_priority + (remainder > 0 ? 1 : 0); |
| 307 | remainder--; |
| 308 | |
| 309 | for (int i = 0; i < count && thread_idx < thread_count; i++) { |
| 310 | contexts[thread_idx++].priority = prio; |
| 311 | } |
| 312 | } |
| 313 | break; |
| 314 | } |
| 315 | } |
| 316 | } |
| 317 | |
| 318 | int locks_stress_test(void) { |
| 319 | summary_stats_t summary = {0}; |
| 320 | |
| 321 | // Initialize actual locks |
| 322 | netdata_mutex_t mutex; |
| 323 | netdata_rwlock_t rwlock; |
| 324 | netdata_mutex_init(&mutex); |
| 325 | netdata_rwlock_init(&rwlock); |
| 326 | |
| 327 | SPINLOCK spinlock = SPINLOCK_INITIALIZER; |
| 328 | RW_SPINLOCK rw_spinlock = RW_SPINLOCK_INITIALIZER; |
| 329 | WAITQ waitq = WAITQ_INITIALIZER; |
| 330 | |
| 331 | void *locks[] = { |
| 332 | &mutex, |
| 333 | &rwlock, |
| 334 | &spinlock, |
| 335 | &rw_spinlock, |
| 336 | &waitq |
| 337 | }; |
| 338 | |
| 339 | // Initialize control structures |
| 340 | lock_control_t controls[NUM_LOCK_TYPES] = { 0 }; |
| 341 | for(int i = 0; i < NUM_LOCK_TYPES; i++) { |
| 342 | // Initialize per-thread condition variables and mutexes |
| 343 | for(int j = 0; j < MAX_THREADS; j++) { |
| 344 | netdata_cond_init(&controls[i].thread_controls[j].cond); |
| 345 | netdata_mutex_init(&controls[i].thread_controls[j].cond_mutex); |
| 346 | controls[i].thread_controls[j].run_flag = 0; |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | // Initialize thread arrays |
| 351 | thread_context_t *threads[NUM_LOCK_TYPES]; |
| 352 | for(int i = 0; i < NUM_LOCK_TYPES; i++) { |
| 353 | threads[i] = calloc(MAX_THREADS, sizeof(thread_context_t)); |
| 354 | if(!threads[i]) { |
| 355 | fprintf(stderr, "Failed to allocate memory for threads\n"); |
| 356 | return 1; |
| 357 | } |
| 358 | |
| 359 | // Initialize thread contexts |
| 360 | for(int j = 0; j < MAX_THREADS; j++) { |
| 361 | threads[i][j] = (thread_context_t){ |
| 362 | .thread_id = j, |
| 363 | .type = i, |
| 364 | .control = &controls[i], |
| 365 | .lock = locks[i] |
| 366 | }; |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | // Create all threads |
| 371 | fprintf(stderr, "Creating threads...\n"); |
| 372 | for(int type = 0; type < NUM_LOCK_TYPES; type++) { |
| 373 | for(int i = 0; i < MAX_THREADS; i++) { |
| 374 | char thr_name[32]; |
| 375 | snprintf(thr_name, sizeof(thr_name), "%s%d", lock_names[type], i); |
| 376 | threads[type][i].thread = nd_thread_create( |
| 377 | thr_name, |
| 378 | NETDATA_THREAD_OPTION_DONT_LOG, |
| 379 | benchmark_thread, |
| 380 | &threads[type][i]); |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | // Run tests with different thread counts |
| 385 | int thread_counts[] = {1, 2, 4, 8, 16, 32, 64}; |
| 386 | |
| 387 | // Warm up the CPU |
| 388 | sleep_usec(100000); |
| 389 | |
| 390 | for(size_t i = 0; i < sizeof(thread_counts)/sizeof(thread_counts[0]); i++) { |
| 391 | int count = thread_counts[i]; |
| 392 | |
| 393 | // Set waitq priorities for this test |
| 394 | set_waitq_priorities(count, threads[LOCK_WAITQ]); |
| 395 | |
| 396 | // Run test for each lock type |
| 397 | for(int type = 0; type < NUM_LOCK_TYPES; type++) { |
| 398 | run_test(lock_names[type], count, threads[type], &controls[type], &summary); |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | // Print the summary table |
| 403 | print_summary(&summary); |
| 404 | |
| 405 | // Signal all threads to exit |
| 406 | fprintf(stderr, "\nStopping threads...\n"); |
| 407 | for(int type = 0; type < NUM_LOCK_TYPES; type++) { |
| 408 | for(int i = 0; i < MAX_THREADS; i++) { |
| 409 | thread_control_t *thread_control = &controls[type].thread_controls[i]; |
| 410 | netdata_mutex_lock(&thread_control->cond_mutex); |
| 411 | thread_control->run_flag = STOP_SIGNAL; |
| 412 | netdata_cond_signal(&thread_control->cond); |
| 413 | netdata_mutex_unlock(&thread_control->cond_mutex); |
| 414 | } |
| 415 | } |
| 416 | |
| 417 | // Join all threads |
| 418 | fprintf(stderr, "\nWaiting for threads to exit...\n"); |
| 419 | for(int type = 0; type < NUM_LOCK_TYPES; type++) { |
| 420 | for(int i = 0; i < MAX_THREADS; i++) { |
| 421 | nd_thread_join(threads[type][i].thread); |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | // Cleanup condition variables and mutexes |
| 426 | for(int type = 0; type < NUM_LOCK_TYPES; type++) { |
| 427 | for(int i = 0; i < MAX_THREADS; i++) { |
| 428 | netdata_cond_destroy(&controls[type].thread_controls[i].cond); |
| 429 | netdata_mutex_destroy(&controls[type].thread_controls[i].cond_mutex); |
| 430 | } |
| 431 | free(threads[type]); |
| 432 | } |
| 433 | |
| 434 | return 0; |
| 435 | } |