CMSDK 2.0.1
Cross-platform C++ base library and SDK for the Psyclone AIOS platform
Loading...
Searching...
No Matches
ProcessMemoryConcFuzz.cpp
Go to the documentation of this file.
1
53
54#include "ProcessMemory.h"
55#include "MemoryManager.h"
56#include "ThreadManager.h"
57#include "UnitTestFramework.h"
58#include <map>
59#include <vector>
60#include <stdlib.h>
61#include <string.h>
62#if defined(__APPLE__)
63 #include <mach-o/dyld.h> // _NSGetExecutablePath, for the tier B re-exec
64#elif !defined(WINDOWS)
65 #include <unistd.h> // readlink("/proc/self/exe")
66#endif
67
68namespace cmlabs {
69
70// ---------------------------------------------------------------------------------
71// Shared helpers
72// ---------------------------------------------------------------------------------
73
75struct ConcRand {
76 uint64 s;
77 ConcRand(uint64 seed) : s(seed ? seed : 0x9E3779B97F4A7C15ULL) {}
78 uint64 next() { s ^= s >> 12; s ^= s << 25; s ^= s >> 27; return s * 2685821657736338717ULL; }
79 uint32 below(uint32 n) { return n ? (uint32)(next() % n) : 0; }
80 bool chance(uint32 pct) { return below(100) < pct; }
81};
82
83static uint32 ConcEnvU32(const char* name, uint32 def) {
84 const char* v = getenv(name);
85 if (!v || !*v) return def;
86 long n = atol(v);
87 return (n > 0) ? (uint32)n : def;
88}
89static bool ConcEnvFlag(const char* name) {
90 const char* v = getenv(name);
91 return v && *v == '1';
92}
93
95static uint32 ConcChecksum(const char* p, uint32 len) {
96 uint32 h = 2166136261u;
97 for (uint32 i = 0; i < len; i++) { h ^= (uint8)p[i]; h *= 16777619u; }
98 return h;
99}
100static void ConcFillPayload(char* buf, uint32 len, uint32 serial) {
101 for (uint32 i = 0; i < len; i++)
102 buf[i] = (char)('A' + ((serial * 31 + i * 7) % 26));
103 buf[len] = 0;
104}
105
108static DataMessage* ConcMakeMsg(uint32 serial, uint32 len, uint32& sumOut) {
109 char* body = new char[len + 1];
110 ConcFillPayload(body, len, serial);
111 sumOut = ConcChecksum(body, len);
112 DataMessage* m = new DataMessage();
113 m->setSerial(serial);
114 m->setInt("cfserial", (int64)serial);
115 m->setInt("cflen", (int64)len);
116 m->setInt("cfsum", (int64)sumOut);
117 m->setString("cfbody", body);
118 delete [] body;
119 return m;
120}
121
122// ---------------------------------------------------------------------------------
123// The set-equality oracle. Concurrency removes total order, so this records per-serial
124// observation counts under a lock and reconciles at the end.
125// ---------------------------------------------------------------------------------
126
129 std::map<uint32, uint32> readCount; // serial -> times returned by a reader
130 std::map<uint32, uint32> expectSum; // serial -> expected payload checksum
131 uint32 written;
132 uint32 corrupt; // payload/len mismatches
133 uint32 torn; // message did not parse as ours at all
134 ConcOracle() : written(0), corrupt(0), torn(0) {}
135
136 void noteWritten(uint32 serial, uint32 sum) {
137 mutex.enter();
138 expectSum[serial] = sum;
139 written++;
140 mutex.leave();
141 }
142
143 void noteRead(DataMessage* m, bool dupInject) {
144 mutex.enter();
145 if (!m || !m->isValid()) { torn++; mutex.leave(); return; }
146 bool ok = false;
147 int64 ser = m->getInt("cfserial", ok);
148 if (!ok) { torn++; mutex.leave(); return; }
149 int64 len = m->getInt("cflen");
150 int64 sum = m->getInt("cfsum");
151 std::string body = m->getAsString("cfbody");
152 uint32 actual = ConcChecksum(body.c_str(), (uint32)body.size());
153 if ((int64)body.size() != len || actual != (uint32)sum)
154 corrupt++;
155 readCount[(uint32)ser]++;
156 // Positive control: count one read twice so the reconciliation MUST report a
157 // duplicate. Proves the oracle can fail rather than always passing.
158 if (dupInject) readCount[(uint32)ser]++;
159 mutex.leave();
160 }
161
162 const char* reconcile(char* det, size_t detSize, bool lostInject) {
163 mutex.enter();
164 uint32 dup = 0, lost = 0, extra = 0;
165 uint32 firstDup = 0, firstLost = 0;
166 std::map<uint32, uint32>::const_iterator it;
167 for (it = expectSum.begin(); it != expectSum.end(); ++it) {
168 std::map<uint32, uint32>::const_iterator r = readCount.find(it->first);
169 uint32 n = (r == readCount.end()) ? 0 : r->second;
170 // Positive control: pretend one written serial was never read.
171 if (lostInject && it->first == 1) n = 0;
172 if (n == 0) { lost++; if (!firstLost) firstLost = it->first; }
173 else if (n > 1) { dup++; if (!firstDup) firstDup = it->first; }
174 }
175 for (it = readCount.begin(); it != readCount.end(); ++it)
176 if (expectSum.find(it->first) == expectSum.end()) extra++;
177 uint32 w = written, c = corrupt, t = torn;
178 mutex.leave();
179
180 snprintf(det, detSize,
181 "written=%u distinct-read=%u LOST=%u DUPLICATED=%u never-written=%u "
182 "corrupt=%u torn=%u (first lost=%u first dup=%u)",
183 w, (uint32)readCount.size(), lost, dup, extra, c, t, firstLost, firstDup);
184 if (t) return "TORN";
185 if (c) return "CORRUPT";
186 if (extra) return "READ_BUT_NEVER_WRITTEN";
187 if (dup) return "DUPLICATED";
188 if (lost) return "LOST";
189 return NULL;
190 }
191};
192
193// ---------------------------------------------------------------------------------
194// Tier A / A' / C / D: threads inside one process, one shared queue.
195// ---------------------------------------------------------------------------------
196
197struct ConcCtx {
200 uint16 procID;
201 uint32 totalMsgs;
202 uint32 payloadFixed; // 0 = mixed sizes
203 uint32 burst; // 0 = steady; else write burst then drain burst
204 uint64 seed;
205 bool resizeStress; // tier C: force repeated growth under readers
206 bool blockingReads; // tier D: readers wait on an empty queue
207 volatile bool writerDone;
208 volatile bool stop;
209 uint32 dupInjectAt; // positive control
210 volatile uint32 readsSeen;
211};
212
214 ConcCtx* c = (ConcCtx*)arg;
215 ConcRand rng(c->seed ^ 0x1517ULL);
216 uint32 serial = 1;
217 while (serial <= c->totalMsgs && !c->stop) {
218 // Burst mode reproduces the real shape: fill to ~burst entries, then let the
219 // readers drain the lot in one go. The real defect always loses the TAIL of such
220 // a burst (serials 61-65 of ~64 published), so a steady trickle may never
221 // construct the state at all.
222 uint32 n = c->burst ? c->burst : 1;
223 for (uint32 i = 0; i < n && serial <= c->totalMsgs && !c->stop; i++) {
224 uint32 len;
225 if (c->payloadFixed) len = c->payloadFixed;
226 else if (c->resizeStress) len = 700 + rng.below(1600);
227 else len = 1 + rng.below(300);
228 uint32 sum = 0;
229 DataMessage* m = ConcMakeMsg(serial, len, sum);
230 // Note the serial BEFORE the write: if addToMsgQ succeeds and the message is
231 // then lost, the oracle must already know it was owed.
232 c->oracle->noteWritten(serial, sum);
233 if (!c->manager->processMemory->addToMsgQ(c->procID, m)) {
234 // A refused write is not a loss - withdraw the expectation so the
235 // reconciliation does not blame the ring for backpressure.
236 c->oracle->mutex.enter();
237 c->oracle->expectSum.erase(serial);
238 c->oracle->written--;
239 c->oracle->mutex.leave();
240 }
241 delete m;
242 serial++;
243 }
244 if (c->burst) {
245 // let readers catch up so the next burst starts from a drained-ish ring
246 for (uint32 g = 0; g < 50 && !c->stop; g++) {
247 if (c->manager->processMemory->getMsgQCount(c->procID) == 0) break;
248 utils::Sleep(1);
249 }
250 }
251 }
252 c->writerDone = true;
253 return 0;
254}
255
257 ConcCtx* c = (ConcCtx*)arg;
258 uint32 idleRounds = 0;
259 while (!c->stop) {
260 // Tier D uses a real timeout so waitForQ takes its blocking path (semaphore wait,
261 // mutex released mid-call, re-enter). Tier A polls with a short timeout so the
262 // readers contend hard on the non-empty fast path instead.
264 c->blockingReads ? 50 : 2);
265 if (m) {
266 idleRounds = 0;
267 uint32 seen = ++c->readsSeen;
268 c->oracle->noteRead(m, c->dupInjectAt && seen == c->dupInjectAt);
269 delete m;
270 continue;
271 }
272 if (c->writerDone) {
273 if (c->manager->processMemory->getMsgQCount(c->procID) == 0 && ++idleRounds > 20)
274 break;
275 }
276 }
277 return 0;
278}
279
283 ConcCtx* c = (ConcCtx*)arg;
284 ConcRand rng(c->seed ^ 0x9E5121ULL);
285 while (!c->stop && !c->writerDone) {
286 // Writing a very large message is the only public way to force the resize path.
287 // It is registered with the oracle like any other message and reconciled the same
288 // way: a resize driver that gets LOST is exactly as interesting as a normal
289 // message that gets lost, so excluding it would discard evidence. Serials are
290 // allocated from a high, non-overlapping range so the writer thread and this
291 // thread can never collide on one.
292 uint32 sum = 0;
293 uint32 serial = 0xF0000000u + (c->readsSeen & 0xFFFFF);
294 DataMessage* big = ConcMakeMsg(serial, 4000 + rng.below(4000), sum);
295 c->oracle->noteWritten(serial, sum);
296 if (!c->manager->processMemory->addToMsgQ(c->procID, big)) {
297 c->oracle->mutex.enter();
298 c->oracle->expectSum.erase(serial);
299 c->oracle->written--;
300 c->oracle->mutex.leave();
301 }
302 delete big;
303 utils::Sleep(2);
304 }
305 return 0;
306}
307
308// ---------------------------------------------------------------------------------
309// Tier drivers
310// ---------------------------------------------------------------------------------
311
312static bool ConcCrossProcessChild(); // tier B child half, defined below
313
315static bool ConcRunThreaded(const char* label, uint32 readers, uint32 msgs,
316 uint32 payload, uint32 burst, uint64 seed, bool resizeStress,
317 bool blockingReads, bool dupSelftest, bool lostSelftest) {
318 MemoryManager* manager = new MemoryManager();
319 if (!manager->create(0)) {
320 unittest::fail("%s: MemoryManager create(0) failed", label);
321 delete manager;
322 return false;
323 }
324
325 ConcOracle oracle;
326 ConcCtx ctx;
327 memset(&ctx, 0, sizeof(ctx));
328 ctx.manager = manager;
329 ctx.oracle = &oracle;
330 ctx.procID = 0; // process 0's MSGQ, same queue Phase 1 uses
331 ctx.totalMsgs = msgs;
332 ctx.payloadFixed = payload;
333 ctx.burst = burst;
334 ctx.seed = seed;
335 ctx.resizeStress = resizeStress;
336 ctx.blockingReads = blockingReads;
337 ctx.writerDone = false;
338 ctx.stop = false;
339 ctx.readsSeen = 0;
340 // Positive control: duplicate one specific read once the run is under way.
341 ctx.dupInjectAt = dupSelftest ? (msgs / 2) : 0;
342
343 std::vector<uint32> tids;
344 uint32 id = 0;
345 for (uint32 i = 0; i < readers; i++) {
346 if (!ThreadManager::CreateThread(ConcReader, &ctx, id)) {
347 unittest::fail("%s: could not create reader thread %u", label, i);
348 ctx.stop = true;
349 delete manager;
350 return false;
351 }
352 tids.push_back(id);
353 }
354 if (resizeStress) {
356 tids.push_back(id);
357 }
358 uint32 wid = 0;
359 if (!ThreadManager::CreateThread(ConcWriter, &ctx, wid)) {
360 unittest::fail("%s: could not create writer thread", label);
361 ctx.stop = true;
362 delete manager;
363 return false;
364 }
365 tids.push_back(wid);
366
367 // Bound the run: readers exit on their own once the writer is done and the queue is
368 // drained, but a hang must not wedge the suite.
369 uint64 start = GetTimeNow();
370 uint32 budgetMs = 60000 * UT_TIME_SCALE;
371 while (GetTimeAgeMS(start) < (int64)budgetMs) {
372 bool anyAlive = false;
373 for (size_t i = 0; i < tids.size(); i++)
374 if (ThreadManager::IsThreadRunning(tids[i])) { anyAlive = true; break; }
375 if (!anyAlive) break;
376 utils::Sleep(20);
377 }
378 ctx.stop = true;
379 for (uint32 g = 0; g < 200; g++) {
380 bool anyAlive = false;
381 for (size_t i = 0; i < tids.size(); i++)
382 if (ThreadManager::IsThreadRunning(tids[i])) { anyAlive = true; break; }
383 if (!anyAlive) break;
384 utils::Sleep(25);
385 }
386
387 char det[512];
388 const char* cls = oracle.reconcile(det, sizeof(det), lostSelftest);
389 // NOTE: an earlier revision also asserted a read-pointer CONTINUITY check recorded
390 // inside waitForQ (BakeLiveBug.md §7). That instrumentation sat on the queue hot path
391 // under the process mutex and was removed when the investigation was filed - it found
392 // no violation on any run, including genuine bakelive failures. The set-equality
393 // oracle below is the durable check: a re-read or a skipped entry shows up as
394 // DUPLICATED or LOST regardless of how the pointer got there.
395 if (cls) {
396 unittest::fail("%s: %s :: %s", label, cls, det);
397 delete manager;
398 return false;
399 }
400 unittest::detail("%s: OK :: %s", label, det);
401 delete manager;
402 return true;
403}
404
413static bool ConcRunCrossProcess(uint32 msgs, uint32 payload, uint64 seed) {
414 // The parent CREATES the segment and writes; a re-exec of this same binary OPENs it by
415 // name and reads. That is the only way to get the REAL contended cross-process mutex
416 // and two independent `header` mappings - the dimension no other tier reaches.
417 //
418 // Reconciliation crosses the boundary through a file: the child appends every serial
419 // it read, and the parent reconciles that list against what it wrote. Coarser than the
420 // in-process oracle (no per-read timing), but it answers the one question that matters:
421 // did anything go missing or double across the process boundary?
422 // ⚠️ NOT a hardcoded "/tmp/...": there is no /tmp on Windows, so the child cannot
423 // create its read-record file and the tier fails with "child produced NO read
424 // records" - i.e. it looks like a cross-process DEFECT when only the harness path
425 // was wrong. (The tier is right to refuse: unmeasured is not clean.) %TEMP% on
426 // Windows, /tmp elsewhere - same rule as PsyTestTempPath / sslTestTempPath.
427#if defined(WINDOWS)
428 const char* tmpEnv = getenv("TEMP");
429 if (!tmpEnv || !*tmpEnv) tmpEnv = getenv("TMP");
430 std::string childList = std::string((tmpEnv && *tmpEnv) ? tmpEnv : ".")
431 + "\\cmsdk_concfuzz_child_reads.txt";
432#else
433 std::string childList = "/tmp/cmsdk_concfuzz_child_reads.txt";
434#endif
435 utils::DeleteAFile(childList.c_str(), true);
436
437 MemoryManager* manager = new MemoryManager();
438 if (!manager->create(0)) {
439 unittest::fail("TIER B: MemoryManager create(0) failed");
440 delete manager;
441 return false;
442 }
443 // Both sides address process 0's MSGQ - the same queue the in-process tiers use - and
444 // the child attaches with connect(0, false), so no port needs passing: system id 0 is
445 // what names the shared segments.
446 // ⚠️ utils::GetCommandLine() and friends return EMPTY here: they are populated by
447 // SetCommandLine(argc, argv), which CMSDKTest's main() never calls. Using them made
448 // this tier fail with "cannot determine own executable path" on every run - correctly,
449 // because the guard refuses to proceed rather than silently skip. Resolve the running
450 // executable from the OS instead, with an env override for awkward setups.
451 std::string exe;
452 const char* exeEnv = getenv("PSY_CONC_EXE");
453 if (exeEnv && *exeEnv)
454 exe = exeEnv;
455 else {
456#if defined(WINDOWS)
457 char buf[4096];
458 DWORD n = GetModuleFileNameA(NULL, buf, sizeof(buf));
459 if (n > 0 && n < sizeof(buf)) { buf[n] = 0; exe = buf; }
460#elif defined(__APPLE__)
461 char buf[4096];
462 uint32_t sz = sizeof(buf);
463 if (_NSGetExecutablePath(buf, &sz) == 0)
464 exe = buf;
465#else
466 char buf[4096];
467 ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
468 if (n > 0) { buf[n] = 0; exe = buf; }
469#endif
470 }
471 if (exe.empty()) {
472 unittest::fail("TIER B: cannot determine own executable path for re-exec "
473 "(set PSY_CONC_EXE=<path to CMSDKTest> to override)");
474 delete manager;
475 return false;
476 }
477 std::string cmd = utils::StringFormat("%s test=processmemory_concfuzz", exe.c_str());
478
479 // Child mode is selected by env so no new command-line parsing is needed.
480 // PSY_CONC_CHILD=read tells the child to open() the segment and drain the queue.
481#if defined(WINDOWS)
482 _putenv_s("PSY_CONC_CHILD", "read");
483 _putenv_s("PSY_CONC_CHILD_OUT", childList.c_str());
484 _putenv_s("PSY_CONC_CHILD_MSGS", utils::StringFormat("%u", msgs).c_str());
485#else
486 setenv("PSY_CONC_CHILD", "read", 1);
487 setenv("PSY_CONC_CHILD_OUT", childList.c_str(), 1);
488 setenv("PSY_CONC_CHILD_MSGS", utils::StringFormat("%u", msgs).c_str(), 1);
489#endif
490 uint32 childPid = utils::NewProcess(cmd.c_str());
491 // ⚠️ Do NOT unset PSY_CONC_CHILD here. NewProcess forks and the child inherits the
492 // environment at EXEC time, not at fork time, so clearing it immediately is a race:
493 // a child that exec's after the unset runs the PARENT path and recurses. Leave it set
494 // for the lifetime of this function; the parent already took its own branch above.
495 if (!childPid) {
496 unittest::fail("TIER B: could not spawn reader child (%s)", cmd.c_str());
497 delete manager;
498 return false;
499 }
500
501 // Give the child time to attach before writing, otherwise the whole burst is written
502 // and drained by nobody and the test measures nothing.
503 utils::Sleep(1500);
504
505 ConcRand rng(seed ^ 0xB10CULL);
506 std::map<uint32, uint32> written; // serial -> checksum
507 for (uint32 serial = 1; serial <= msgs; serial++) {
508 uint32 len = payload ? payload : (1 + rng.below(300));
509 uint32 sum = 0;
510 DataMessage* m = ConcMakeMsg(serial, len, sum);
511 if (manager->processMemory->addToMsgQ(0, m))
512 written[serial] = sum;
513 delete m;
514 if ((serial % 64) == 0)
515 utils::Sleep(1); // let the child drain, keeps the ring cycling
516 }
517
518 // Wait for the child to finish or time out.
519 uint64 start = GetTimeNow();
520 int rc = 0;
521 bool exited = false;
522 while (GetTimeAgeMS(start) < (int64)(30000 * UT_TIME_SCALE)) {
523 if (utils::GetProcessStatus(childPid, rc) == PROC_TERMINATED) { exited = true; break; }
524 utils::Sleep(50);
525 }
526 if (!exited) {
527 utils::EndProcess(childPid);
528 unittest::detail("TIER B: child did not exit in time; terminated (partial result)");
529 }
530
531 // Reconcile: every serial written exactly once in the child's list.
532 std::string data = utils::ReadAFileString(childList.c_str());
533 std::vector<std::string> lines = utils::TextListSplitLines(data.c_str(), false, true);
534 std::map<uint32, uint32> readCount;
535 uint32 badSum = 0;
536 for (size_t i = 0; i < lines.size(); i++) {
537 if (lines[i].find("R|") != 0) continue;
538 std::vector<std::string> f = utils::TextListSplit(lines[i].c_str(), "|", true, true);
539 if (f.size() < 3) continue;
540 uint32 s = (uint32)strtoul(f[1].c_str(), NULL, 10);
541 uint32 c = (uint32)strtoul(f[2].c_str(), NULL, 10);
542 readCount[s]++;
543 std::map<uint32, uint32>::const_iterator w = written.find(s);
544 if (w != written.end() && w->second != c) badSum++;
545 }
546 uint32 lost = 0, dup = 0, extra = 0, firstLost = 0;
547 for (std::map<uint32, uint32>::const_iterator it = written.begin(); it != written.end(); ++it) {
548 std::map<uint32, uint32>::const_iterator r = readCount.find(it->first);
549 uint32 n = (r == readCount.end()) ? 0 : r->second;
550 if (n == 0) { lost++; if (!firstLost) firstLost = it->first; }
551 else if (n > 1) dup++;
552 }
553 for (std::map<uint32, uint32>::const_iterator it = readCount.begin(); it != readCount.end(); ++it)
554 if (written.find(it->first) == written.end()) extra++;
555
556 delete manager;
557
558 if (!lines.size()) {
559 // An empty list means the child never reported - that is UNMEASURED, not clean.
560 unittest::fail("TIER B: child produced NO read records (%s) - the cross-process "
561 "path is UNMEASURED, not clean. child exited=%d rc=%d",
562 childList.c_str(), (int)exited, (int)rc);
563 return false;
564 }
565 if (lost || dup || extra || badSum) {
566 unittest::fail("TIER B (cross-process): written=%u child-read-distinct=%u LOST=%u "
567 "DUPLICATED=%u never-written=%u badChecksum=%u (first lost=%u)",
568 (uint32)written.size(), (uint32)readCount.size(), lost, dup, extra, badSum,
569 firstLost);
570 return false;
571 }
572 unittest::detail("TIER B (cross-process): OK :: written=%u child-read-distinct=%u "
573 "LOST=0 DUPLICATED=0 badChecksum=0 (real cross-process mutex + two header mappings)",
574 (uint32)written.size(), (uint32)readCount.size());
575 return true;
576}
577
581 const char* outPath = getenv("PSY_CONC_CHILD_OUT");
582 uint32 want = ConcEnvU32("PSY_CONC_CHILD_MSGS", 20000);
583 if (!outPath || !*outPath)
584 return false;
585
586 MemoryManager* manager = new MemoryManager();
587 // open() attaches to the segment the PARENT created - this is the whole point of the
588 // tier: a second independent mapping and the real named cross-process mutex.
589 if (!manager->connect(0, false)) {
590 delete manager;
591 return false;
592 }
593 uint32 got = 0, idle = 0;
594 std::string batch;
595 while (got < want && idle < 600) {
596 DataMessage* m = manager->processMemory->waitForMsgQ(0, 50);
597 if (!m) { idle++; continue; }
598 idle = 0;
599 bool ok = false;
600 int64 ser = m->getInt("cfserial", ok);
601 std::string body = m->getAsString("cfbody");
602 uint32 sum = ConcChecksum(body.c_str(), (uint32)body.size());
603 if (ok)
604 batch += utils::StringFormat("R|%llu|%u\n", (unsigned long long)ser, sum);
605 delete m;
606 got++;
607 // Batch the appends: one write per message would make the child the bottleneck
608 // and could itself change the timing under test.
609 if (batch.size() > 8192) {
610 utils::AppendToAFile(outPath, batch.c_str(), (uint32)batch.size());
611 batch.clear();
612 }
613 }
614 if (!batch.empty())
615 utils::AppendToAFile(outPath, batch.c_str(), (uint32)batch.size());
616 delete manager;
617 return true;
618}
619
621 // Tier B child half: this same binary re-exec'd with PSY_CONC_CHILD=read. Must be the
622 // FIRST thing checked - the child must not run the tier drivers and recurse.
623 const char* childMode = getenv("PSY_CONC_CHILD");
624 if (childMode && *childMode) {
625 bool ok = ConcCrossProcessChild();
626 // The child's verdict reaches the parent through the read-record file, not through
627 // this return value, so a false here only marks the child's own failure to attach.
628 return ok;
629 }
630
631 const char* tierEnv = getenv("PSY_CONC_TIER");
632 std::string tier = (tierEnv && *tierEnv) ? tierEnv : "ALL";
633 uint32 readers = ConcEnvU32("PSY_CONC_READERS", 4);
634 uint32 msgs = ConcEnvU32("PSY_CONC_MSGS", 20000);
635 uint32 burst = ConcEnvU32("PSY_CONC_BURST", 64);
636 uint64 seed = (uint64)ConcEnvU32("PSY_CONC_SEED", 0x2000);
637 bool dupSt = ConcEnvFlag("PSY_CONC_DUPSELFTEST");
638 bool lostSt = ConcEnvFlag("PSY_CONC_LOSTSELFTEST");
639 // Default payload ~2280 bytes: the real bake wire size is 2314 and message size is
640 // what determines how many entries fit before a wrap/resize, so matching it matters
641 // more than randomising it.
642 uint32 payload = ConcEnvU32("PSY_CONC_PAYLOAD", 2280);
643
644 bool all = (tier == "ALL");
645 bool ok = true;
646
647 if (ok && (all || tier == "A")) {
648 unittest::progress(10, "tier A: multi-reader contention on one queue");
649 ok = ConcRunThreaded("TIER A (multi-reader)", readers, msgs, payload,
650 0 /*steady*/, seed, false, false, dupSt, lostSt);
651 }
652 if (ok && (all || tier == "A2")) {
653 unittest::progress(30, "tier A': burst-drain replay of the real shape");
654 ok = ConcRunThreaded("TIER A' (burst-drain)", readers, msgs, payload,
655 burst, seed, false, false, dupSt, lostSt);
656 }
657 if (ok && (all || tier == "B")) {
658 unittest::progress(50, "tier B: cross-process");
659 ok = ConcRunCrossProcess(msgs, payload, seed);
660 }
661 if (ok && (all || tier == "C")) {
662 unittest::progress(65, "tier C: resize under concurrent readers");
663 ok = ConcRunThreaded("TIER C (resize-under-read)", readers, msgs, payload,
664 0, seed, true /*resizeStress*/, false, dupSt, lostSt);
665 }
666 if (ok && (all || tier == "D")) {
667 unittest::progress(85, "tier D: blocking waitForQ path");
668 ok = ConcRunThreaded("TIER D (blocking reads)", readers, msgs, payload,
669 burst, seed, false, true /*blockingReads*/, dupSt, lostSt);
670 }
671
672 unittest::progress(100, "done");
673 return ok;
674}
675
676} // namespace cmlabs
Central shared-memory manager for a Psyclone node: master segment, per-subsystem shared maps and the ...
Shared-memory process ("space") table plus per-process message queues.
Process-wide thread registry and lifecycle manager: the concurrency core of CMSDK.
Small, dependency-free unit test harness used by all CMSDK object tests.
#define UT_TIME_SCALE
#define THREAD_RET
Definition Utils.h:127
#define THREAD_FUNCTION_CALL
Definition Utils.h:129
#define THREAD_ARG
Definition Utils.h:130
The central Psyclone data container: a self-contained binary message with typed, named user entries.
bool setString(const char *key, const char *value)
setString(const char* key, const char* value)
bool getInt(const char *key, int64 &value)
getInt(const char* key, int64& value)
bool setInt(const char *key, int64 value)
setInt(const char* key, int64 value)
std::string getAsString(const char *key)
getAsString(const char* key)
bool isValid()
isValid() Checks that the message memory block exists and carries the current-format object id (DATAM...
bool setSerial(uint64 serial)
setSerial(uint64 serial)
Top-level facade of the shared-memory subsystem for one process.
ProcessMemory * processMemory
Accessor for the process table and per-process queues.
bool create(uint16 sysID, uint32 slotCount=100000, uint16 binCount=2, uint32 minBlockSize=1024, uint32 maxBlockSize=64 *1024, uint64 initSize=50000000L, uint64 maxSize=1000000000L, bool force=false)
Create all shared segments for a new node instance (master process only).
bool connect(uint16 sysID, bool isMaster)
Attach this process to an existing node's shared segments.
bool addToMsgQ(uint16 procID, DataMessage *msg)
Enqueue on the data-message queue.
uint32 getMsgQCount(uint16 procID)
DataMessage * waitForMsgQ(uint16 procID, uint32 timeout)
Wait on the data-message queue.
static bool ConcFuzzTest()
Phase 2: CONCURRENT + cross-process fuzz of the ring (ProcessMemoryConcFuzz.cpp).
static bool CreateThread(THREAD_FUNCTION func, void *args, uint32 &newID, uint32 reqID=0)
Create a new native thread and start it immediately.
static bool IsThreadRunning(uint32 id)
Check whether the thread is still alive at the OS level.
Recursive mutual-exclusion lock, optionally named for cross-process use.
Definition Utils.h:463
bool leave()
Release the mutex.
Definition Utils.cpp:1330
bool enter()
Block until the mutex is acquired.
Definition Utils.cpp:1158
uint64 GetTimeNow()
Return the current absolute time (µs since year 0) according to the TMC.
Definition PsyTime.cpp:69
int32 GetTimeAgeMS(uint64 t)
Age of a timestamp relative to now, in milliseconds.
Definition PsyTime.cpp:35
uint32 NewProcess(const char *cmdline, const char *initdir=NULL, const char *title=NULL, int16 x=-1, int16 y=-1, int16 w=-1, int16 h=-1)
Launch a detached child process.
Definition Utils.cpp:4387
bool EndProcess(uint32 proc)
Forcibly terminate a child process.
Definition Utils.cpp:4602
bool Sleep(uint32 ms)
Suspend the calling thread.
Definition Utils.cpp:3121
uint8 GetProcessStatus(uint32 proc, int &returncode)
Poll a child started with NewProcess().
Definition Utils.cpp:4519
#define PROC_TERMINATED
Definition Utils.h:1276
void fail(const char *fmt,...)
Set an explanatory reason shown on the FAIL line.
void detail(const char *fmt,...)
Verbose-only indented diagnostic line (shown only when verbose=1).
void progress(int percent, const char *action)
Report progress with a short description of the current action.
std::string ReadAFileString(std::string filename)
Read an entire file into a std::string.
Definition Utils.cpp:8238
bool AppendToAFile(const char *filename, const char *data, uint32 length, bool binary=false)
Append to a file, creating it if missing.
Definition Utils.cpp:8346
std::vector< std::string > TextListSplit(const char *text, const char *split, bool keepEmpty=true, bool autoTrim=false)
Split text on a separator.
Definition Utils.cpp:7952
bool DeleteAFile(const char *filename, bool force)
Delete a file.
Definition Utils.cpp:8374
std::vector< std::string > TextListSplitLines(const char *text, bool keepEmpty=true, bool autoTrim=false)
Split text into lines, handling both \n and \r\n.
Definition Utils.cpp:7775
std::string StringFormat(const char *format,...)
printf into a std::string.
Definition Utils.cpp:8067
static uint32 ConcChecksum(const char *p, uint32 len)
FNV-1a — identical to Phase 1's so payload checks are comparable.
static THREAD_RET THREAD_FUNCTION_CALL ConcReader(THREAD_ARG arg)
static bool ConcRunCrossProcess(uint32 msgs, uint32 payload, uint64 seed)
Tier B: two OS processes on one segment.
static bool ConcCrossProcessChild()
Tier B child half: open() the existing segment and drain, appending every serial read.
static THREAD_RET THREAD_FUNCTION_CALL ConcWriter(THREAD_ARG arg)
static THREAD_RET THREAD_FUNCTION_CALL ConcResizer(THREAD_ARG arg)
Tier C helper: grow the segment repeatedly while readers are live, so resize() unmaps and repoints he...
static DataMessage * ConcMakeMsg(uint32 serial, uint32 len, uint32 &sumOut)
Build one message carrying its own serial + payload, so a reader can re-derive what it SHOULD have re...
static bool ConcEnvFlag(const char *name)
static uint32 ConcEnvU32(const char *name, uint32 def)
static void ConcFillPayload(char *buf, uint32 len, uint32 serial)
static bool ConcRunThreaded(const char *label, uint32 readers, uint32 msgs, uint32 payload, uint32 burst, uint64 seed, bool resizeStress, bool blockingReads, bool dupSelftest, bool lostSelftest)
Run one in-process tier.
std::map< uint32, uint32 > readCount
void noteRead(DataMessage *m, bool dupInject)
Record one read.
std::map< uint32, uint32 > expectSum
const char * reconcile(char *det, size_t detSize, bool lostInject)
void noteWritten(uint32 serial, uint32 sum)
Deterministic xorshift64* — same as Phase 1 so seeds behave comparably.