├── .gitignore ├── LICENSE.txt ├── README.md ├── jobsystem.h └── test └── jobsystemtest ├── jobsystemtest.cpp ├── jobsystemtest.sln ├── jobsystemtest.vcxproj ├── jobsystemtest.vcxproj.filters └── targetver.h /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.obj 6 | *.pdb 7 | *.ilk 8 | 9 | # [BLM] Visual Studio Local 10 | *.suo 11 | *.sdf 12 | *.opensdf 13 | *.ncb 14 | *.user 15 | *.tlog 16 | *.log 17 | *.lastbuildstate 18 | Debug/ 19 | *.ib_pdb_index 20 | *.successfulbuild 21 | *.unsuccessfulbuild 22 | bin/ 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2018 - Bill Merrill 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## C++ Job System (Requires C++11 but nothing newer than that) 2 | 3 | Implements a typical job system with features I've found useful in practice (Games, mostly). 4 | 5 | Tested on Windows 10 and Ubuntu 18. 6 | 7 | Features include: 8 | - Simple interfaces for submitting, monitoring, and canceling jobs. 9 | - Easy-to-configure affinities and policies to match workers to cores, as well as jobs to specific workers, which is useful for maximizing data-sharing and cache performance by constraining jobs operating in the same areas of memory to the same cluster(s). 10 | - Basic work-stealing algorithm, enabling internal load-balancing between worker threads; currently lock-based. 11 | - Full support for job dependency graphs. 12 | - External thread-assist: Any thread outside of the worker pool can temporarily assist in job processing until some particular job is complete. A surprising percentage of production job systems lack this, leaving the producer thread to block/wait for jobs to be completed by the worker pool, which depending on your thread configuration can be a huge waste. 13 | - Bare-bones but useful "profiler" showing a timeline for each worker with utilization over time. Jobs can be submitted with an optional "debug character", which allows one to visualize when specific jobs ran, for how long, and on which workers. 14 | - Simple, straightforward, easy to modify. 15 | - No silly super modern C++, so does not rely on brand new compiler versions and potentially buggy STL implementations. 16 | - MIT permissive license. 17 | 18 | **Bug fixes, suggestions, complaints, and any knowledge I lack** is more than welcome. This isn't something I've iterated on heavily, and certainly wouldn't consider myself an expert in job system research. 19 | -------------------------------------------------------------------------------- /jobsystem.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef COMMONJOBSYSTEM_HEADER 3 | #define COMMONJOBSYSTEM_HEADER 4 | 5 | #pragma once 6 | 7 | #if defined(WIN32) || defined(WIN64) 8 | # define WINDOWS 9 | #elif defined(__unix__) 10 | # define LINUX 11 | #endif 12 | 13 | #ifdef WINDOWS 14 | # define NOMINMAX 15 | # define STRICT 16 | # define WIN32_LEAN_AND_MEAN 17 | # include 18 | #endif 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | namespace jobsystem 35 | { 36 | inline uint64_t GetBit(uint64_t n) 37 | { 38 | return static_cast(1) << n; 39 | } 40 | 41 | inline size_t CountBits(uint64_t n) 42 | { 43 | size_t bits = 0; 44 | while(n) 45 | { 46 | bits += n & 1; 47 | n >>= 1; 48 | } 49 | return bits; 50 | } 51 | 52 | typedef std::function JobDelegate; ///< Structure of callbacks that can be requested as jobs. 53 | 54 | typedef uint64_t affinity_t; 55 | 56 | static const affinity_t kAffinityAllBits = static_cast(~0); 57 | 58 | /** 59 | * Global system components. 60 | */ 61 | std::atomic s_nextJobId; ///< Job ID assignment for debugging / profiling. 62 | std::mutex s_signalLock; ///< Global mutex for worker signaling. 63 | std::condition_variable s_signalThreads; ///< Global condition var for worker signaling. 64 | std::atomic s_activeWorkers; 65 | 66 | inline affinity_t CalculateSafeWorkerAffinity(size_t workerIndex, size_t workerCount) 67 | { 68 | affinity_t affinity = kAffinityAllBits; // Set all bits so jobs with affinities out of range can still be processed. 69 | affinity &= ~(workerCount - 1); // Wipe bits within valid range. 70 | affinity |= GetBit(workerIndex); // Set worker-specific bit. 71 | 72 | return affinity; 73 | } 74 | 75 | /** 76 | * Offers access to the state of job. 77 | * In particular, callers can use the Wait() function to ensure a given job is complete, 78 | * or Cancel(). 79 | * Note, however, that doing so is not good practice with job systems. If no hardware threads 80 | * are available to process a given job, you can stall the caller for significant time. 81 | * 82 | * Internally, the state manages dependencies as well as atomics describing the status of the job. 83 | */ 84 | typedef std::shared_ptr JobStatePtr; 85 | 86 | class JobState 87 | { 88 | private: 89 | 90 | friend class JobSystemWorker; 91 | friend class JobManager; 92 | 93 | std::atomic m_cancel; ///< Is the job pending cancellation? 94 | std::atomic m_ready; ///< Has the job been marked as ready for processing? 95 | 96 | std::vector m_dependants; ///< List of dependent jobs. 97 | std::atomic m_dependencies; ///< Number of outstanding dependencies. 98 | 99 | std::atomic m_done; ///< Has the job executed to completion? 100 | std::condition_variable m_doneSignal; 101 | std::mutex m_doneMutex; 102 | 103 | affinity_t m_workerAffinity; ///< Option to limit execution to specific worker threads / cores. 104 | 105 | size_t m_jobId; ///< Debug/profiling ID. 106 | char m_debugChar; ///< Debug character for profiling display. 107 | 108 | 109 | void SetQueued() 110 | { 111 | m_done.store(false, std::memory_order_release); 112 | } 113 | 114 | void SetDone() 115 | { 116 | JOBSYSTEM_ASSERT(!IsDone()); 117 | 118 | for (const JobStatePtr& dependant : m_dependants) 119 | { 120 | dependant->m_dependencies.fetch_sub(1, std::memory_order_relaxed); 121 | } 122 | 123 | std::lock_guard lock(m_doneMutex); 124 | m_done.store(true, std::memory_order_release); 125 | m_doneSignal.notify_all(); 126 | } 127 | 128 | bool AwaitingCancellation() const 129 | { 130 | return m_cancel.load(std::memory_order_relaxed); 131 | } 132 | 133 | public: 134 | 135 | JobState() 136 | : m_debugChar(0) 137 | { 138 | m_jobId = s_nextJobId++; 139 | m_workerAffinity = kAffinityAllBits; 140 | 141 | m_dependencies.store(0, std::memory_order_release); 142 | m_cancel.store(false, std::memory_order_release); 143 | m_ready.store(false, std::memory_order_release); 144 | m_done.store(false, std::memory_order_release); 145 | } 146 | 147 | ~JobState() {} 148 | 149 | JobState& SetReady() 150 | { 151 | JOBSYSTEM_ASSERT(!IsDone()); 152 | 153 | m_cancel.store(false, std::memory_order_relaxed); 154 | m_ready.store(true, std::memory_order_release); 155 | 156 | s_signalThreads.notify_all(); 157 | 158 | return *this; 159 | } 160 | 161 | JobState& Cancel() 162 | { 163 | JOBSYSTEM_ASSERT(!IsDone()); 164 | 165 | m_cancel.store(true, std::memory_order_relaxed); 166 | 167 | return *this; 168 | } 169 | 170 | JobState& AddDependant(JobStatePtr dependant) 171 | { 172 | JOBSYSTEM_ASSERT(m_dependants.end() == std::find(m_dependants.begin(), m_dependants.end(), dependant)); 173 | 174 | m_dependants.push_back(dependant); 175 | 176 | dependant->m_dependencies.fetch_add(1, std::memory_order_relaxed); 177 | 178 | return *this; 179 | } 180 | 181 | JobState& SetWorkerAffinity(affinity_t affinity) 182 | { 183 | m_workerAffinity = affinity ? affinity : kAffinityAllBits; 184 | 185 | return *this; 186 | } 187 | 188 | bool IsDone() const 189 | { 190 | return m_done.load(std::memory_order_acquire); 191 | } 192 | 193 | bool Wait(size_t maxWaitMicroseconds = 0) 194 | { 195 | if (!IsDone()) 196 | { 197 | std::unique_lock lock(m_doneMutex); 198 | 199 | if (maxWaitMicroseconds == 0) 200 | { 201 | m_doneSignal.wait(lock, 202 | [this]() 203 | { 204 | return IsDone(); 205 | } 206 | ); 207 | } 208 | else 209 | { 210 | m_doneSignal.wait_for(lock, std::chrono::microseconds(maxWaitMicroseconds)); 211 | } 212 | } 213 | 214 | return IsDone(); 215 | } 216 | 217 | bool AreDependenciesMet() const 218 | { 219 | if (!m_ready.load(std::memory_order_acquire)) 220 | { 221 | return false; 222 | } 223 | 224 | if (m_dependencies.load(std::memory_order_relaxed) > 0) 225 | { 226 | return false; 227 | } 228 | 229 | return true; 230 | } 231 | 232 | bool HasDependencies() const 233 | { 234 | return (m_dependencies.load(std::memory_order_relaxed) > 0); 235 | } 236 | }; 237 | 238 | /** 239 | * Represents an entry in a job queue. 240 | * - A delegate to invoke 241 | * - Internal job state 242 | */ 243 | struct JobQueueEntry 244 | { 245 | JobDelegate m_delegate; ///< Delegate to invoke for the job. 246 | JobStatePtr m_state; ///< Pointer to job state. 247 | }; 248 | 249 | /** 250 | * Descriptor for a given job worker thread, to be provided by the host application. 251 | */ 252 | struct JobWorkerDescriptor 253 | { 254 | JobWorkerDescriptor(const char* name = "JobSystemWorker", affinity_t cpuAffinity = affinity_t(~0), bool enableWorkSteeling = true) 255 | : m_name(name) 256 | , m_cpuAffinity(cpuAffinity) 257 | , m_enableWorkStealing(enableWorkSteeling) 258 | { 259 | } 260 | 261 | std::string m_name; ///< Worker name, for debug/profiling displays. 262 | affinity_t m_cpuAffinity; ///< Thread affinity. Defaults to all cores. 263 | bool m_enableWorkStealing : 1; ///< Enable queue-sharing between workers? 264 | }; 265 | 266 | /** 267 | * Job events (for tracking/debugging). 268 | */ 269 | enum EJobEvent 270 | { 271 | eJobEvent_JobPopped, ///< A job was popped from a queue. 272 | eJobEvent_JobStart, ///< A job is about to start. 273 | eJobEvent_JobDone, ///< A job just completed. 274 | eJobEvent_JobRun, ///< A job has been completed. 275 | eJobEvent_JobRunAssisted, ///< A job has been completed through outside assistance. 276 | eJobEvent_JobStolen, ///< A worker has stolen a job from another worker. 277 | eJobEvent_WorkerAwoken, ///< A worker has been awoken. 278 | eJobEvent_WorkerUsed, ///< A worker has been utilized. 279 | }; 280 | 281 | typedef std::function JobEventObserver; ///< Delegate definition for job event observation. 282 | 283 | typedef std::deque JobQueue; ///< Data structure to represent job queue. 284 | 285 | /** 286 | * High-res clock based on windows performance counter. Supports STL chrono interfaces. 287 | */ 288 | using TimePoint = std::chrono::high_resolution_clock::time_point; 289 | TimePoint ProfileClockNow() 290 | { 291 | return std::chrono::high_resolution_clock::now(); 292 | } 293 | 294 | /** 295 | * Tracking each job's start/end times in a per-worker timeline, for debugging/profiling. 296 | */ 297 | class ProfilingTimeline 298 | { 299 | public: 300 | 301 | struct TimelineEntry 302 | { 303 | uint64_t jobId; ///< ID of the job that generated this timeline entry. 304 | TimePoint start; ///< Job start time. 305 | TimePoint end; ///< Job end time. 306 | char debugChar; ///< Job's debug character for profiling display. 307 | 308 | std::string description; ///< Timeline entry description. 309 | }; 310 | 311 | typedef std::vector TimelineEntries; 312 | 313 | TimelineEntries m_entries; //< List of timeline entries for this thread. 314 | }; 315 | 316 | /** 317 | * Represents a worker thread. 318 | * - Owns a job queue 319 | * - Implements work-stealing from other workers 320 | */ 321 | class JobSystemWorker 322 | { 323 | friend class JobManager; 324 | 325 | public: 326 | 327 | JobSystemWorker(const JobWorkerDescriptor& desc, const JobEventObserver& eventObserver) 328 | : m_allWorkers(nullptr) 329 | , m_workerCount(0) 330 | , m_workerIndex(0) 331 | , m_desc(desc) 332 | , m_eventObserver(eventObserver) 333 | , m_hasShutDown(false) 334 | { 335 | } 336 | 337 | void Start(size_t index, JobSystemWorker** allWorkers, size_t workerCount) 338 | { 339 | m_allWorkers = allWorkers; 340 | m_workerCount = workerCount; 341 | m_workerIndex = index; 342 | 343 | m_thread = std::thread(&JobSystemWorker::WorkerThreadProc, this); 344 | } 345 | 346 | void Shutdown() 347 | { 348 | m_stop.store(true, std::memory_order_relaxed); 349 | 350 | while (!m_hasShutDown.load(std::memory_order_acquire)) 351 | { 352 | s_signalThreads.notify_all(); 353 | 354 | std::this_thread::sleep_for(std::chrono::microseconds(100)); 355 | } 356 | 357 | if (m_hasShutDown.load(std::memory_order_acquire)) 358 | { 359 | m_thread.join(); 360 | } 361 | } 362 | 363 | JobStatePtr PushJob(JobDelegate delegate) 364 | { 365 | JobQueueEntry entry = { delegate, std::make_shared() }; 366 | entry.m_state->SetQueued(); 367 | 368 | { 369 | std::lock_guard queueLock(m_queueLock); 370 | m_queue.insert(m_queue.begin(), entry); 371 | } 372 | 373 | return entry.m_state; 374 | } 375 | 376 | private: 377 | 378 | void NotifyEventObserver(const JobQueueEntry& job, EJobEvent event, uint64_t workerIndex, size_t jobId = 0) 379 | { 380 | #ifdef JOBSYSTEM_ENABLE_PROFILING 381 | 382 | if (m_eventObserver) 383 | { 384 | m_eventObserver(job, event, workerIndex, jobId); 385 | } 386 | 387 | #endif // JOBSYSTEM_ENABLE_PROFILING 388 | } 389 | 390 | bool PopJobFromQueue(JobQueue& queue, JobQueueEntry& job, bool& hasUnsatisfiedDependencies, affinity_t workerAffinity) 391 | { 392 | for (auto jobIter = queue.begin(); jobIter != queue.end();) 393 | { 394 | const JobQueueEntry& candidate = (*jobIter); 395 | 396 | if ((workerAffinity & candidate.m_state->m_workerAffinity) != 0) 397 | { 398 | if (candidate.m_state->AwaitingCancellation()) 399 | { 400 | candidate.m_state->SetDone(); 401 | jobIter = queue.erase(jobIter); 402 | 403 | continue; 404 | } 405 | else if (candidate.m_state->AreDependenciesMet()) 406 | { 407 | job = candidate; 408 | queue.erase(jobIter); 409 | 410 | NotifyEventObserver(job, eJobEvent_JobPopped, m_workerIndex); 411 | 412 | return true; 413 | } 414 | } 415 | 416 | hasUnsatisfiedDependencies = true; 417 | ++jobIter; 418 | } 419 | 420 | return false; 421 | } 422 | 423 | bool PopNextJob(JobQueueEntry& job, bool& hasUnsatisfiedDependencies, bool useWorkStealing, affinity_t workerAffinity) 424 | { 425 | bool foundJob = false; 426 | 427 | { 428 | std::lock_guard queueLock(m_queueLock); 429 | foundJob = PopJobFromQueue(m_queue, job, hasUnsatisfiedDependencies, workerAffinity); 430 | } 431 | 432 | if (!foundJob && useWorkStealing) 433 | { 434 | for (size_t i = 0; foundJob == false && i < m_workerCount; ++i) 435 | { 436 | JOBSYSTEM_ASSERT(m_allWorkers[i]); 437 | JobSystemWorker& worker = *m_allWorkers[i]; 438 | 439 | { 440 | std::lock_guard queueLock(worker.m_queueLock); 441 | foundJob = PopJobFromQueue(worker.m_queue, job, hasUnsatisfiedDependencies, workerAffinity); 442 | } 443 | } 444 | 445 | if (foundJob) 446 | { 447 | NotifyEventObserver(job, eJobEvent_JobStolen, m_workerIndex); 448 | } 449 | } 450 | 451 | return foundJob; 452 | } 453 | 454 | void SetThreadName(const char* name) 455 | { 456 | (void)name; 457 | #if defined(WINDOWS) 458 | typedef struct tagTHREADNAME_INFO 459 | { 460 | unsigned long dwType; // must be 0x1000 461 | const char* szName; // pointer to name (in user addr space) 462 | unsigned long dwThreadID; // thread ID (-1=caller thread) 463 | unsigned long dwFlags; // reserved for future use, must be zero 464 | } THREADNAME_INFO; 465 | 466 | THREADNAME_INFO threadName; 467 | threadName.dwType = 0x1000; 468 | threadName.szName = name; 469 | threadName.dwThreadID = GetCurrentThreadId(); 470 | threadName.dwFlags = 0; 471 | __try 472 | { 473 | RaiseException(0x406D1388, 0, sizeof(threadName) / sizeof(ULONG_PTR), (ULONG_PTR*)&threadName); 474 | } 475 | __except (EXCEPTION_CONTINUE_EXECUTION) 476 | { 477 | } 478 | #elif defined(LINUX) 479 | pthread_setname_np(pthread_self(), name); 480 | #endif 481 | } 482 | 483 | void WorkerThreadProc() 484 | { 485 | SetThreadName(m_desc.m_name.c_str()); 486 | 487 | #if defined(WINDOWS) 488 | SetThreadAffinityMask(m_thread.native_handle(), m_desc.m_cpuAffinity); 489 | #elif defined(LINUX) 490 | cpu_set_t cpuset; 491 | CPU_ZERO(&cpuset); 492 | for(size_t i = 0; i < sizeof(m_desc.m_cpuAffinity) * 8; ++i) 493 | { 494 | if ((1 << i) & m_desc.m_cpuAffinity) 495 | { 496 | CPU_SET(i, &cpuset); 497 | } 498 | } 499 | #endif 500 | 501 | const affinity_t workerAffinity = CalculateSafeWorkerAffinity(m_workerIndex, m_workerCount); 502 | 503 | while (true) 504 | { 505 | JobQueueEntry job; 506 | { 507 | std::unique_lock signalLock(s_signalLock); 508 | 509 | bool hasUnsatisfiedDependencies; 510 | 511 | while (!m_stop.load(std::memory_order_relaxed) && 512 | !PopNextJob(job, hasUnsatisfiedDependencies, m_desc.m_enableWorkStealing, workerAffinity)) 513 | { 514 | s_signalThreads.wait(signalLock); 515 | NotifyEventObserver(job, eJobEvent_WorkerAwoken, m_workerIndex); 516 | } 517 | } 518 | 519 | if (m_stop.load(std::memory_order_relaxed)) 520 | { 521 | m_hasShutDown.store(true, std::memory_order_release); 522 | 523 | break; 524 | } 525 | 526 | s_activeWorkers.fetch_add(1, std::memory_order_acq_rel); 527 | { 528 | NotifyEventObserver(job, eJobEvent_WorkerUsed, m_workerIndex); 529 | 530 | NotifyEventObserver(job, eJobEvent_JobStart, m_workerIndex, job.m_state->m_jobId); 531 | job.m_delegate(); 532 | NotifyEventObserver(job, eJobEvent_JobDone, m_workerIndex); 533 | 534 | job.m_state->SetDone(); 535 | 536 | NotifyEventObserver(job, eJobEvent_JobRun, m_workerIndex); 537 | 538 | s_signalThreads.notify_one(); 539 | } 540 | s_activeWorkers.fetch_sub(1, std::memory_order_acq_rel); 541 | } 542 | } 543 | 544 | std::thread m_thread; ///< Thread instance for worker. 545 | std::atomic m_stop; ///< Has a stop been requested? 546 | std::atomic m_hasShutDown; ///< Has the worker completed shutting down? 547 | 548 | mutable std::mutex m_queueLock; ///< Mutex to guard worker queue. 549 | JobQueue m_queue; ///< Queue containing requested jobs. 550 | 551 | JobSystemWorker** m_allWorkers; ///< Pointer to array of all workers, for queue-sharing / work-stealing. 552 | size_t m_workerCount; ///< Number of total workers (size of m_allWorkers array). 553 | size_t m_workerIndex; ///< This worker's index within m_allWorkers. 554 | 555 | JobEventObserver m_eventObserver; ///< Observer of job-related events occurring on this worker. 556 | JobWorkerDescriptor m_desc; ///< Descriptor/configuration of this worker. 557 | }; 558 | 559 | /** 560 | * Descriptor for configuring the job manager. 561 | * - Contains descriptor for each worker 562 | */ 563 | struct JobManagerDescriptor 564 | { 565 | std::vector m_workers; ///< Configurations for all workers that should be spawned by JobManager. 566 | }; 567 | 568 | /** 569 | * Manages job workers, and acts as the primary interface to the job queue. 570 | */ 571 | class JobManager 572 | { 573 | private: 574 | 575 | void Observer(const JobQueueEntry& job, EJobEvent event, uint64_t workerIndex, size_t jobId = 0) 576 | { 577 | #ifdef JOBSYSTEM_ENABLE_PROFILING 578 | switch (event) 579 | { 580 | case eJobEvent_JobRun: 581 | { 582 | ++m_jobsRun; 583 | } 584 | break; 585 | 586 | case eJobEvent_JobStolen: 587 | { 588 | ++m_jobsStolen; 589 | } 590 | break; 591 | 592 | case eJobEvent_JobRunAssisted: 593 | { 594 | ++m_jobsAssisted; 595 | ++m_jobsRun; 596 | } 597 | break; 598 | 599 | case eJobEvent_WorkerAwoken: 600 | { 601 | m_awokenMask |= GetBit(workerIndex); 602 | } 603 | break; 604 | 605 | case eJobEvent_WorkerUsed: 606 | { 607 | m_usedMask |= GetBit(workerIndex); 608 | } 609 | break; 610 | 611 | case eJobEvent_JobStart: 612 | { 613 | ProfilingTimeline& timeline = workerIndex < m_workers.size() ? m_timelines[workerIndex] : m_timelines[m_workers.size()]; 614 | ProfilingTimeline::TimelineEntry entry; 615 | entry.jobId = jobId; 616 | entry.start = ProfileClockNow(); 617 | entry.debugChar = job.m_state ? job.m_state->m_debugChar : 0; 618 | timeline.m_entries.push_back(entry); 619 | } 620 | break; 621 | 622 | case eJobEvent_JobDone: 623 | { 624 | ProfilingTimeline& timeline = workerIndex < m_workers.size() ? m_timelines[workerIndex] : m_timelines[m_workers.size()]; 625 | ProfilingTimeline::TimelineEntry& entry = timeline.m_entries.back(); 626 | entry.end = ProfileClockNow(); 627 | } 628 | break; 629 | 630 | case eJobEvent_JobPopped: 631 | { 632 | if (!m_hasPushedJob) 633 | { 634 | m_firstJobTime = ProfileClockNow(); 635 | m_hasPushedJob = true; 636 | } 637 | } 638 | break; 639 | } 640 | #endif // JOBSYSTEM_ENABLE_PROFILING 641 | } 642 | 643 | public: 644 | 645 | JobManager() 646 | : m_jobsRun(0) 647 | , m_jobsStolen(0) 648 | , m_usedMask(0) 649 | , m_awokenMask(0) 650 | , m_nextRoundRobinWorkerIndex(0) 651 | , m_timelines(nullptr) 652 | , m_firstJobTime() 653 | { 654 | 655 | } 656 | 657 | ~JobManager() 658 | { 659 | DumpProfilingResults(); 660 | 661 | JoinWorkersAndShutdown(); 662 | } 663 | 664 | bool Create(const JobManagerDescriptor& desc) 665 | { 666 | JoinWorkersAndShutdown(); 667 | 668 | m_desc = desc; 669 | 670 | const size_t workerCount = desc.m_workers.size(); 671 | m_workers.reserve(workerCount); 672 | 673 | #ifdef JOBSYSTEM_ENABLE_PROFILING 674 | 675 | m_timelines = new ProfilingTimeline[workerCount + 1]; 676 | m_hasPushedJob = false; 677 | 678 | #endif // JOBSYSTEM_ENABLE_PROFILING 679 | 680 | const JobEventObserver observer = std::bind( 681 | &JobManager::Observer, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4); 682 | 683 | // Create workers. We don't spawn the threads yet. 684 | for (size_t i = 0; i < workerCount; ++i) 685 | { 686 | const JobWorkerDescriptor& workerDesc = desc.m_workers[i]; 687 | 688 | JobSystemWorker* worker = new JobSystemWorker(workerDesc, observer); 689 | m_workers.push_back(worker); 690 | } 691 | 692 | // Start the workers (includes spawning threads). Each worker maintains 693 | // understanding of what other workers exist, for work-stealing purposes. 694 | for (size_t i = 0; i < workerCount; ++i) 695 | { 696 | m_workers[i]->Start(i, &m_workers[0], workerCount); 697 | } 698 | 699 | return !m_workers.empty(); 700 | } 701 | 702 | JobStatePtr AddJob(JobDelegate delegate, char debugChar = 0) 703 | { 704 | JobStatePtr state = nullptr; 705 | 706 | // \todo - workers should maintain a tls pointer to themselves, so we can push 707 | // directly into our own queue. 708 | 709 | if (!m_workers.empty()) 710 | { 711 | // Add round-robin style. Note that work-stealing helps load-balance, 712 | // if it hasn't been disabled. If it has we may need to consider a 713 | // smarter scheme here. 714 | state = m_workers[m_nextRoundRobinWorkerIndex]->PushJob(delegate); 715 | state->m_debugChar = debugChar; 716 | 717 | m_nextRoundRobinWorkerIndex = (m_nextRoundRobinWorkerIndex + 1) % m_workers.size(); 718 | } 719 | 720 | return state; 721 | } 722 | 723 | void AssistUntilJobDone(JobStatePtr state) 724 | { 725 | JOBSYSTEM_ASSERT(state->m_ready.load(std::memory_order_acquire)); 726 | 727 | const affinity_t workerAffinity = kAffinityAllBits; 728 | 729 | // Steal jobs from workers until the specified job is done. 730 | while (!state->IsDone()) 731 | { 732 | JOBSYSTEM_ASSERT(!m_workers.empty()); 733 | 734 | JobQueueEntry job; 735 | bool hasUnsatisfiedDependencies; 736 | 737 | if (m_workers[0]->PopNextJob(job, hasUnsatisfiedDependencies, true, workerAffinity)) 738 | { 739 | Observer(job, eJobEvent_JobStart, m_workers.size(), job.m_state->m_jobId); 740 | job.m_delegate(); 741 | Observer(job, eJobEvent_JobDone, m_workers.size()); 742 | 743 | job.m_state->SetDone(); 744 | 745 | Observer(job, eJobEvent_JobRunAssisted, 0); 746 | 747 | s_signalThreads.notify_one(); 748 | } 749 | } 750 | } 751 | 752 | void AssistUntilDone() 753 | { 754 | JOBSYSTEM_ASSERT(!m_workers.empty()); 755 | 756 | // Steal and run jobs from workers until all queues are exhausted. 757 | 758 | const affinity_t workerAffinity = kAffinityAllBits; 759 | 760 | JobQueueEntry job; 761 | bool foundBusyWorker = true; 762 | 763 | while (foundBusyWorker) 764 | { 765 | foundBusyWorker = false; 766 | 767 | for (JobSystemWorker* worker : m_workers) 768 | { 769 | if (worker->PopNextJob(job, foundBusyWorker, false, workerAffinity)) 770 | { 771 | Observer(job, eJobEvent_JobStart, m_workers.size(), job.m_state->m_jobId); 772 | job.m_delegate(); 773 | Observer(job, eJobEvent_JobDone, m_workers.size()); 774 | 775 | job.m_state->SetDone(); 776 | 777 | Observer(job, eJobEvent_JobRunAssisted, 0); 778 | 779 | foundBusyWorker = true; 780 | s_signalThreads.notify_one(); 781 | break; 782 | } 783 | } 784 | } 785 | 786 | for (JobSystemWorker* worker : m_workers) 787 | { 788 | if (!worker->m_queue.empty()) 789 | { 790 | JOBSYSTEM_ASSERT(0); 791 | } 792 | } 793 | } 794 | 795 | void JoinWorkersAndShutdown(bool finishJobs = false) 796 | { 797 | if (finishJobs) 798 | { 799 | AssistUntilDone(); 800 | } 801 | 802 | // Tear down each worker. Un-popped jobs may still reside in the queues at this point 803 | // if finishJobs = false. 804 | // Don't destruct workers yet, in case someone's in the process of work-stealing. 805 | for (size_t i = 0, n = m_workers.size(); i < n; ++i) 806 | { 807 | JOBSYSTEM_ASSERT(m_workers[i]); 808 | m_workers[i]->Shutdown(); 809 | } 810 | 811 | // Destruct all workers. 812 | std::for_each(m_workers.begin(), m_workers.end(), [](JobSystemWorker* worker) { delete worker; }); 813 | m_workers.clear(); 814 | 815 | #ifdef JOBSYSTEM_ENABLE_PROFILING 816 | 817 | delete[] m_timelines; 818 | m_timelines = nullptr; 819 | 820 | #endif // JOBSYSTEM_ENABLE_PROFILING 821 | } 822 | 823 | private: 824 | 825 | size_t m_nextRoundRobinWorkerIndex; ///< Index of the worker to receive the next requested job, round-robin style. 826 | 827 | std::atomic m_jobsRun; ///< Counter to track # of jobs run. 828 | std::atomic m_jobsAssisted; ///< Counter to track # of jobs run via external Assist*(). 829 | std::atomic m_jobsStolen; ///< Counter to track # of jobs stolen from another worker's queue. 830 | std::atomic m_usedMask; ///< Mask with bits set according to the IDs of the jobs that have executed jobs. 831 | std::atomic m_awokenMask; ///< Mask with bits set according to the IDs of the jobs that have been awoken at least once. 832 | 833 | private: 834 | 835 | JobManagerDescriptor m_desc; ///< Descriptor/configuration of the job manager. 836 | 837 | bool m_hasPushedJob; ///< For profiling - has a job been pushed yet? 838 | TimePoint m_firstJobTime; ///< For profiling - when was the first job pushed? 839 | ProfilingTimeline* m_timelines; ///< For profiling - a ProfilingTimeline entry for each worker, plus an additional entry to represent the Assist thread. 840 | 841 | std::vector m_workers; ///< Storage for worker instances. 842 | 843 | void DumpProfilingResults() 844 | { 845 | #ifdef JOBSYSTEM_ENABLE_PROFILING 846 | 847 | AssistUntilDone(); 848 | 849 | auto now = ProfileClockNow(); 850 | auto totalNS = std::chrono::duration_cast(now - m_firstJobTime).count(); 851 | 852 | std::this_thread::sleep_for(std::chrono::milliseconds(10)); 853 | 854 | const size_t workerCount = m_workers.size(); 855 | 856 | printf( 857 | "\n[Job System Statistics]\n" 858 | "Jobs Run: %8d\n" // May be < jobs submitted 859 | "Jobs Stolen: %8d\n" 860 | "Jobs Assisted: %8d\n" 861 | "Workers Used: %8lu\n" 862 | "Workers Awoken: %8lu\n" 863 | , 864 | m_jobsRun.load(std::memory_order_acquire), 865 | m_jobsStolen.load(std::memory_order_acquire), 866 | m_jobsAssisted.load(std::memory_order_acquire), 867 | CountBits(m_usedMask.load(std::memory_order_acquire)), 868 | CountBits(m_awokenMask.load(std::memory_order_acquire))); 869 | 870 | printf("\n[Worker Profiling Results]\n%.3f total ms\n\nTimeline (approximated):\n\n", double(totalNS) / 1000000); 871 | 872 | const char* busySymbols = "abcdefghijklmn"; 873 | const size_t busySymbolCount = strlen(busySymbols); 874 | 875 | for (size_t workerIndex = 0; workerIndex < workerCount + 1; ++workerIndex) 876 | { 877 | ProfilingTimeline& timeline = m_timelines[workerIndex]; 878 | 879 | const char* name = (workerIndex < workerCount) ? m_workers[workerIndex]->m_desc.m_name.c_str() : "[Assist]"; 880 | 881 | const size_t bufferSize = 200; 882 | char buffer[bufferSize]; 883 | snprintf(buffer, sizeof(buffer), "%20s: ", name); 884 | 885 | const size_t nameLen = strlen(buffer); 886 | const size_t remaining = bufferSize - nameLen - 2; 887 | 888 | for (size_t nameIdx = nameLen; nameIdx < bufferSize - 2; ++nameIdx) 889 | { 890 | buffer[nameIdx] = '-'; 891 | } 892 | 893 | buffer[bufferSize - 2] = '\n'; 894 | buffer[bufferSize - 1] = 0; 895 | 896 | for (ProfilingTimeline::TimelineEntry& entry : timeline.m_entries) 897 | { 898 | const auto startNs = std::chrono::duration_cast(entry.start - m_firstJobTime).count(); 899 | const auto endNs = std::chrono::duration_cast(entry.end - m_firstJobTime).count(); 900 | 901 | const double startPercent = (double(startNs) / double(totalNS)); 902 | const double endPercent = (double(endNs) / double(totalNS)); 903 | 904 | const char jobCharacter = (entry.debugChar != 0) ? entry.debugChar : busySymbols[entry.jobId % busySymbolCount]; 905 | 906 | const size_t startIndex = nameLen + std::min(remaining - 1, size_t(startPercent * double(remaining))); 907 | size_t endIndex = nameLen + std::min(remaining - 1, size_t(endPercent * double(remaining))); 908 | 909 | size_t shift = 0; 910 | 911 | while (buffer[startIndex + shift] != '-' && startIndex + shift < bufferSize - 3 && endIndex + shift < bufferSize - 3) 912 | { 913 | ++shift; 914 | } 915 | 916 | endIndex -= std::min(endIndex - startIndex, size_t(shift)); 917 | 918 | for (size_t i = startIndex + shift; i <= endIndex + shift; ++i) 919 | { 920 | JOBSYSTEM_ASSERT(i < bufferSize - 2); 921 | buffer[i] = jobCharacter; 922 | } 923 | } 924 | 925 | printf("%s", buffer); 926 | } 927 | 928 | printf("\n"); 929 | 930 | #endif // JOBSYSTEM_ENABLE_PROFILING 931 | } 932 | }; 933 | 934 | /** 935 | * Helper for building complex job/dependency chains logically. 936 | * 937 | * e.g. 938 | * 939 | * jobsystem::JobManager jobManager; 940 | * ... 941 | * jobsystem::JobChainBuilder<128>(jobManager) 942 | * .Do(something, 'a') 943 | * .Then() 944 | * .Do(somethingAfterThat, 'b') 945 | * .Then() 946 | * .Together() 947 | * .Do(parallelThing1, 'c') 948 | * .Do(parallelThing2, 'd') 949 | * .Do(parallelThing3, 'e') 950 | * .Close() 951 | * .Then() 952 | * .Do(finalThing, 'F') 953 | * .Go() 954 | * .WaitForAll(); 955 | ** --- parallelThing1 --- 956 | * / \ 957 | * something -> somethingAfterThat -> --- parallelThing2 ---- -> finalThing 958 | * \ / 959 | * --- parallelThing3 --- 960 | * etc... 961 | * 962 | */ 963 | template 964 | class JobChainBuilder 965 | { 966 | public: 967 | 968 | struct Node 969 | { 970 | Node() : isGroup(false), groupDependency(nullptr) {} 971 | ~Node() {} 972 | 973 | Node* groupDependency; 974 | JobStatePtr job; 975 | bool isGroup; 976 | }; 977 | 978 | Node* AllocNode() 979 | { 980 | if (m_nextNodeIndex >= MaxJobNodes) 981 | return nullptr; 982 | 983 | Node* node = &m_nodePool[m_nextNodeIndex++]; 984 | *node = Node(); 985 | 986 | return node; 987 | } 988 | 989 | JobChainBuilder(JobManager& manager) 990 | : mgr(manager) 991 | { 992 | Reset(); 993 | 994 | // Push a sentinel (root) node. 995 | m_stack.push_back(AllocNode()); 996 | } 997 | 998 | void Reset() 999 | { 1000 | m_allJobs.clear(); 1001 | m_stack.clear(); 1002 | 1003 | m_last = nullptr; 1004 | m_dependency = nullptr; 1005 | m_nextNodeIndex = 0; 1006 | m_failed = false; 1007 | } 1008 | 1009 | JobChainBuilder& Together(char debugChar = 0) 1010 | { 1011 | if (Node* item = AllocNode()) 1012 | { 1013 | item->isGroup = true; 1014 | item->groupDependency = m_dependency; 1015 | 1016 | item->job = mgr.AddJob([]() {}, debugChar); 1017 | 1018 | m_allJobs.push_back(item->job); 1019 | 1020 | m_last = item; 1021 | m_dependency = nullptr; 1022 | 1023 | m_stack.push_back(item); 1024 | } 1025 | else 1026 | { 1027 | Fail(); 1028 | } 1029 | 1030 | return *this; 1031 | } 1032 | 1033 | JobChainBuilder& Do(JobDelegate delegate, char debugChar = 0) 1034 | { 1035 | Node* owner = m_stack.back(); 1036 | 1037 | if (Node* item = AllocNode()) 1038 | { 1039 | item->job = mgr.AddJob(delegate, debugChar); 1040 | 1041 | m_allJobs.push_back(item->job); 1042 | 1043 | if (m_dependency) 1044 | { 1045 | m_dependency->job->AddDependant(item->job); 1046 | m_dependency = nullptr; 1047 | } 1048 | 1049 | if (owner && owner->isGroup) 1050 | { 1051 | item->job->AddDependant(owner->job); 1052 | 1053 | if (owner->groupDependency) 1054 | { 1055 | owner->groupDependency->job->AddDependant(item->job); 1056 | } 1057 | } 1058 | 1059 | m_last = item; 1060 | } 1061 | else 1062 | { 1063 | Fail(); 1064 | } 1065 | 1066 | return *this; 1067 | } 1068 | 1069 | JobChainBuilder& Then() 1070 | { 1071 | m_dependency = m_last; 1072 | m_last = (m_dependency) ? m_dependency->groupDependency : nullptr; 1073 | 1074 | return *this; 1075 | } 1076 | 1077 | JobChainBuilder& Close() 1078 | { 1079 | if (!m_stack.empty()) 1080 | { 1081 | Node* owner = m_stack.back(); 1082 | if (owner->isGroup) 1083 | { 1084 | m_last = owner; 1085 | } 1086 | } 1087 | 1088 | m_dependency = nullptr; 1089 | 1090 | if (m_stack.size() > 1) 1091 | { 1092 | m_stack.pop_back(); 1093 | } 1094 | 1095 | return *this; 1096 | } 1097 | 1098 | JobChainBuilder& Go() 1099 | { 1100 | if (m_allJobs.empty()) 1101 | { 1102 | return *this; 1103 | } 1104 | 1105 | Then(); 1106 | Do([]() {}, 'J'); 1107 | m_joinJob = m_allJobs.back(); 1108 | 1109 | for (JobStatePtr& job : m_allJobs) 1110 | { 1111 | job->SetReady(); 1112 | } 1113 | 1114 | return *this; 1115 | } 1116 | 1117 | void Fail() 1118 | { 1119 | for (JobStatePtr& job : m_allJobs) 1120 | { 1121 | job->Cancel(); 1122 | } 1123 | 1124 | m_allJobs.clear(); 1125 | m_failed = true; 1126 | } 1127 | 1128 | bool Failed() const 1129 | { 1130 | return m_failed; 1131 | } 1132 | 1133 | void WaitForAll() 1134 | { 1135 | if (m_joinJob) 1136 | { 1137 | m_joinJob->Wait(); 1138 | } 1139 | } 1140 | 1141 | void AssistAndWaitForAll() 1142 | { 1143 | mgr.AssistUntilJobDone(m_joinJob); 1144 | } 1145 | 1146 | JobManager& mgr; ///< Job manager to submit jobs to. 1147 | 1148 | Node m_nodePool[MaxJobNodes]; ///< Pool of chain nodes (on the stack). The only necessary output of this system is jobs. Nodes are purely internal. 1149 | size_t m_nextNodeIndex; ///< Next free item in the pool. 1150 | 1151 | std::vector m_stack; ///< Internal stack to track groupings. 1152 | std::vector m_allJobs; ///< All jobs created by the builder, to be readied on completion. 1153 | 1154 | Node* m_last; ///< Last job to be pushed, to handle setting up dependencies after Then() calls. 1155 | Node* m_dependency; ///< Any job promoted to a dependency for the next job, as dicated by Then(). 1156 | 1157 | JobStatePtr m_joinJob; ///< Final join job that callers can wait on to complete the batch. 1158 | 1159 | bool m_failed; ///< Did an error occur during creation of the DAG? 1160 | }; 1161 | 1162 | 1163 | } // namespace jobsystem 1164 | 1165 | #endif // COMMONJOBSYSTEM_HEADER 1166 | -------------------------------------------------------------------------------- /test/jobsystemtest/jobsystemtest.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | // jobsystem settings 7 | #define JOBSYSTEM_ENABLE_PROFILING ///< Enables worker/job profiling, and an ascii profile dump on shutdown. 8 | #define JOBSYSTEM_ASSERT(...) assert(__VA_ARGS__) ///< Directs internal system asserts to app-specific assert mechanism. 9 | 10 | // jobsystem include 11 | #include 12 | 13 | #include "dex_profiling/ICapture.h" 14 | #include "dex_profiling/ICapture.cpp" 15 | 16 | int main() 17 | { 18 | jobsystem::JobManagerDescriptor jobManagerDesc; 19 | 20 | const size_t kWorkerCount = 16; 21 | for (size_t i = 0; i < kWorkerCount; ++i) 22 | { 23 | jobManagerDesc.m_workers.emplace_back("Worker"); 24 | } 25 | 26 | jobsystem::JobManager jobManager; 27 | if (!jobManager.Create(jobManagerDesc)) 28 | { 29 | return 1; 30 | } 31 | 32 | const size_t kNumParallelJobs = 1000; 33 | const size_t kItersPerJob = 100000; 34 | 35 | float floats[64]; 36 | 37 | auto something = [&]() { 38 | DEX_ICAPTURE_ZONE(~0, 0, "something"); 39 | for (size_t i = 0; i < kItersPerJob; ++i) 40 | floats[0] *= 5.f; 41 | }; 42 | 43 | auto somethingAfterThat = [&]() { 44 | DEX_ICAPTURE_ZONE(~0, 0, "somethingAfterThat"); 45 | for (size_t i = 0; i < kItersPerJob; ++i) 46 | floats[8] *= 5.f; 47 | }; 48 | 49 | auto parallelThing1 = [&]() { 50 | DEX_ICAPTURE_ZONE(~0, 0, "parallelThing1"); 51 | for (size_t i = 0; i < kItersPerJob; ++i) 52 | floats[16] *= 5.f; 53 | }; 54 | 55 | auto parallelThing2 = [&]() { 56 | DEX_ICAPTURE_ZONE(~0, 0, "parallelThing2"); 57 | for (size_t i = 0; i < kItersPerJob; ++i) 58 | floats[24] *= 5.f; 59 | }; 60 | 61 | auto parallelThing3 = [&]() { 62 | DEX_ICAPTURE_ZONE(~0, 0, "parallelThing3"); 63 | for (size_t i = 0; i < kItersPerJob; ++i) 64 | floats[32] *= 5.f; 65 | }; 66 | 67 | auto finalThing = [&]() { 68 | DEX_ICAPTURE_ZONE(~0, 0, "finalThing"); 69 | for (size_t i = 0; i < kItersPerJob; ++i) 70 | floats[40] *= 5.f; 71 | }; 72 | 73 | dex::ICaptureStart("./capture_1_proc.icap"); 74 | 75 | jobsystem::JobChainBuilder<10000> builder(jobManager); 76 | 77 | // Run a couple jobs in succession. 78 | builder 79 | .Do(something, 'a') 80 | .Then() 81 | .Do(somethingAfterThat, 'b') 82 | .Then() 83 | 84 | // Run 1k jobs in parallel. 85 | .Together(); 86 | for (size_t i = 0; i < kNumParallelJobs; ++i) 87 | { 88 | const char c = 'A' + (char)(i % ('z' - 'A')); 89 | builder.Do(parallelThing1, c); 90 | } 91 | 92 | // Run a final "join" job. 93 | builder 94 | .Close() 95 | .Then() 96 | .Do(finalThing, 'Z'); 97 | 98 | // Run the jobs and assist until complete. 99 | builder 100 | .Go() 101 | .AssistAndWaitForAll(); 102 | 103 | dex::ICaptureStop(); 104 | 105 | return builder.Failed() ? 1 : 0; 106 | } 107 | -------------------------------------------------------------------------------- /test/jobsystemtest/jobsystemtest.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27703.2018 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "jobsystemtest", "jobsystemtest.vcxproj", "{4FC5B6FE-D227-4EE8-B4A5-9244C946387B}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B}.Debug|x64.ActiveCfg = Debug|x64 17 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B}.Debug|x64.Build.0 = Debug|x64 18 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B}.Debug|x86.ActiveCfg = Debug|Win32 19 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B}.Debug|x86.Build.0 = Debug|Win32 20 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B}.Release|x64.ActiveCfg = Release|x64 21 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B}.Release|x64.Build.0 = Release|x64 22 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B}.Release|x86.ActiveCfg = Release|Win32 23 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {7AC13BB2-4A80-476B-BED1-80C9BC907AB7} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /test/jobsystemtest/jobsystemtest.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 15.0 23 | {4FC5B6FE-D227-4EE8-B4A5-9244C946387B} 24 | Win32Proj 25 | jobsystemtest 26 | 10.0.15063.0 27 | 28 | 29 | 30 | Application 31 | true 32 | v140 33 | Unicode 34 | 35 | 36 | Application 37 | false 38 | v140 39 | true 40 | Unicode 41 | 42 | 43 | Application 44 | true 45 | v140 46 | Unicode 47 | 48 | 49 | Application 50 | false 51 | v140 52 | true 53 | Unicode 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | true 75 | 76 | 77 | true 78 | 79 | 80 | false 81 | 82 | 83 | false 84 | 85 | 86 | 87 | Use 88 | Level4 89 | Disabled 90 | true 91 | WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) 92 | true 93 | ..\..\ 94 | true 95 | 96 | 97 | Console 98 | true 99 | 100 | 101 | 102 | 103 | Use 104 | Level4 105 | Disabled 106 | true 107 | _DEBUG;_CONSOLE;%(PreprocessorDefinitions);WIN64 108 | true 109 | ..\..\ 110 | true 111 | 112 | 113 | Console 114 | true 115 | 116 | 117 | 118 | 119 | Use 120 | Level4 121 | MaxSpeed 122 | true 123 | true 124 | true 125 | WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) 126 | true 127 | ..\..\ 128 | true 129 | 130 | 131 | Console 132 | true 133 | true 134 | true 135 | 136 | 137 | 138 | 139 | Use 140 | Level4 141 | MaxSpeed 142 | true 143 | true 144 | true 145 | NDEBUG;_CONSOLE;%(PreprocessorDefinitions);WIN64 146 | true 147 | ..\..\ 148 | true 149 | 150 | 151 | Console 152 | true 153 | true 154 | true 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | Create 166 | Create 167 | Create 168 | Create 169 | 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /test/jobsystemtest/jobsystemtest.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;ipp;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | {8ecfb947-9ae4-47d8-bb82-1b97648e189d} 18 | 19 | 20 | 21 | 22 | Header Files 23 | 24 | 25 | Header Files 26 | 27 | 28 | jobsystem 29 | 30 | 31 | 32 | 33 | Source Files 34 | 35 | 36 | Source Files 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/jobsystemtest/targetver.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/delscorcho/basic-job-system/a593e466a5ee408b40161cf7553c040c344e1ce4/test/jobsystemtest/targetver.h --------------------------------------------------------------------------------