local U=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\106\101\115\116-\103\108\111\098\097\108\115")local S=U.beforeEach local Z=U.expect local q=U.describe local i=U.it local r=U.jest local x local N local w local b local D local l local m local A local function H(U)if U==N then return"\073\109\109\101\100\105\097\116\101"elseif U==w then return"\085\115\101\114-\098\108\111\099\107\105\110\103"elseif U==b then return"\078\111\114\109\097\108"elseif U==D then return"\076\111\119"elseif U==l then return"\073\100\108\101"else return nil end end q("\083\099\104\101\100\117\108\101\114",function()i("\112\114\111\102\105\108\105\110\103 \065\080\073\115 \097\114\101 \110\111\116 \097\118\097\105\108\097\098\108\101",function()local U=require("\046\046\047\083\099\104\101\100\117\108\101\114\070\101\097\116\117\114\101\070\108\097\103\115")U.enableProfiling=false x=(require("\046\046\047\083\099\104\101\100\117\108\101\114"))();(Z(x.unstable_Profiling)).toBe(nil)end)S(function()r.resetModules()r.useFakeTimers()local U=require("\046\046\047\083\099\104\101\100\117\108\101\114\070\101\097\116\117\114\101\070\108\097\103\115")U.enableProfiling=true x=require("\046\046\047\117\110\115\116\097\098\108\101\095\109\111\099\107")N=x.unstable_ImmediatePriority w=x.unstable_UserBlockingPriority b=x.unstable_NormalPriority D=x.unstable_LowPriority l=x.unstable_IdlePriority m=x.unstable_scheduleCallback A=x.unstable_cancelCallback end)local U=1 local q=2 local O=3 local o=4 local f=5 local y=6 local J=7 local G=8 local function Y()local S=x.unstable_Profiling.stopLoggingProfilingEvents()if S==nil then return"\040\101\109\112\116\121 \112\114\111\102\105\108\101\041"end local Z={table.unpack(S)}local i={}local r={}local N=true local w=1 while w<=#Z do local S=Z[w][1]local x=Z[w][2]if S==0 then break elseif S==U then local U=Z[w][3]local S=Z[w][4]local q={id=U;priorityLevel=S,label=nil,start=x;end_=-1,exitStatus=nil,runs={}}i[U]=q w+=1 elseif S==q then if N then error("\084\097\115\107 \099\097\110\110\111\116 \067\111\109\112\108\101\116\101 \111\117\116\115\105\100\101 \116\104\101 \119\111\114\107 \108\111\111\112\046")end local U=Z[w][3]local S=i[U]if S==nil then error("\084\097\115\107 \100\111\101\115 \110\111\116 \101\120\105\115\116\046")end S.end_=x S.exitStatus="\099\111\109\112\108\101\116\101\100"w+=1 elseif S==O then if N then error("\084\097\115\107 \099\097\110\110\111\116 \069\114\114\111\114 \111\117\116\115\105\100\101 \116\104\101 \119\111\114\107 \108\111\111\112\046")end local U=Z[w][3]local S=i[U]if S==nil then error("\084\097\115\107 \100\111\101\115 \110\111\116 \101\120\105\115\116\046")end S.end_=x S.exitStatus="\101\114\114\111\114\101\100"w+=1 elseif S==o then local U=Z[w][3]local S=i[U]if S==nil then error("\084\097\115\107 \100\111\101\115 \110\111\116 \101\120\105\115\116\046")end S.end_=x S.exitStatus="\099\097\110\099\101\108\101\100"w+=1 elseif S==f or S==y then if N then error("\084\097\115\107 \099\097\110\110\111\116 \082\117\110 \111\114 \089\105\101\108\100 \111\117\116\115\105\100\101 \116\104\101 \119\111\114\107 \108\111\111\112\046")end local U=Z[w][3]local S=i[U]if S==nil then error("\084\097\115\107 \100\111\101\115 \110\111\116 \101\120\105\115\116\046")end table.insert(S.runs,x)w+=1 elseif S==J then if N then error("\083\099\104\101\100\117\108\101\114 \099\097\110\110\111\116 \083\117\115\112\101\110\100 \111\117\116\115\105\100\101 \116\104\101 \119\111\114\107 \108\111\111\112\046")end N=true table.insert(r,x)w+=1 elseif S==G then if not N then error("\083\099\104\101\100\117\108\101\114 \099\097\110\110\111\116 \082\101\115\117\109\101 \105\110\115\105\100\101 \116\104\101 \119\111\114\107 \108\111\111\112\046")end N=false table.insert(r,x)w+=1 else error("\085\110\107\110\111\119\110 \105\110\115\116\114\117\099\116\105\111\110 \116\121\112\101\058 "+S)end end local b=30 local D=50000 local l=""local m="\033\033\033 \077\097\105\110 \116\104\114\101\097\100              "local A=""local Y=true for U,S in r do local Z=S/D for U=1,Z-string.len(A),1 do A..=(function()if Y then return"\088"end return"\095"end)()end Y=not Y end l..=m..("\226\148\130"..(A.."\n"))local p={}for U,S in i do table.insert(p,S)end table.sort(p,function(U,S)return S.priorityLevel>U.priorityLevel end)for U,S in p do local Z=S.label if Z==nil then Z="\084\097\115\107"end local q=string.format("\084\097\115\107 \037\100 \091\037\115\093",S.id,H(S.priorityLevel))for U=1,(b-string.len(q))-1,1 do q..=" "end local i=""for U=1,S.start/D,1 do i..=" "end local r=false for U,S in S.runs do local Z=S/D for U=1,Z-string.len(i),1 do i..=(function()if r then return"\088"end return"\095"end)()end r=not r end local x=S.end_/D for U=1,x-string.len(i),1 do i..=(function()if r then return"\088"end return"\095"end)()end if S.exitStatus~="\099\111\109\112\108\101\116\101\100"then i..="\079 "..(S.exitStatus or"")end l..=q..("\226\148\130"..(i.."\n"))end return"\n"..l end i("\099\114\101\097\116\101\115 \097 \098\097\115\105\099 \102\108\097\109\101\103\114\097\112\104",function()x.unstable_Profiling.startLoggingProfilingEvents()x.unstable_advanceTime(100)m(b,function()x.unstable_advanceTime(300)x.unstable_yieldValue("\089\105\101\108\100 \049")m(w,function()x.unstable_yieldValue("\089\105\101\108\100 \050")x.unstable_advanceTime(300)end,{label="\066\097\114"})x.unstable_advanceTime(100)x.unstable_yieldValue("\089\105\101\108\100 \051")return function()x.unstable_yieldValue("\089\105\101\108\100 \052")x.unstable_advanceTime(300)end end,{label="\070\111\111"});(Z(x)).toFlushAndYieldThrough({"\089\105\101\108\100 \049";"\089\105\101\108\100 \051"})x.unstable_advanceTime(100);(Z(x)).toFlushAndYield({"\089\105\101\108\100 \050";"\089\105\101\108\100 \052"});(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\088\088\095\095\095\095\095\095\095\095\088\088\095\095\095\095\095\095\095\095\095\095\095\095\n\084\097\115\107 \050 \091\085\115\101\114-\098\108\111\099\107\105\110\103\093       \226\148\130        \095\095\095\095\088\088\088\088\088\088\n\084\097\115\107 \049 \091\078\111\114\109\097\108\093              \226\148\130  \088\088\088\088\088\088\088\088\095\095\095\095\095\095\095\095\088\088\088\088\088\088\n")end)i("\109\097\114\107\115 \119\104\101\110 \097 \084\097\115\107 \105\115 \099\097\110\099\101\108\101\100",function()x.unstable_Profiling.startLoggingProfilingEvents()local U=m(b,function()x.unstable_yieldValue("\089\105\101\108\100 \049")x.unstable_advanceTime(300)x.unstable_yieldValue("\089\105\101\108\100 \050")return function()x.unstable_yieldValue("\067\111\110\116\105\110\117\097\116\105\111\110")x.unstable_advanceTime(200)end end);(Z(x)).toFlushAndYieldThrough({"\089\105\101\108\100 \049","\089\105\101\108\100 \050"})x.unstable_advanceTime(100)A(U)x.unstable_advanceTime(1000);(Z(x)).toFlushWithoutYielding();(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\095\095\095\095\095\095\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\n\084\097\115\107 \049 \091\078\111\114\109\097\108\093              \226\148\130\088\088\088\088\088\088\095\095\079 \099\097\110\099\101\108\101\100\n")end)i("\109\097\114\107\115 \119\104\101\110 \097 \116\097\115\107 \101\114\114\111\114\115",function()x.unstable_Profiling.startLoggingProfilingEvents()m(b,function()x.unstable_advanceTime(300)error("\079\111\112\115")end);(Z(x)).toFlushAndThrow("\079\111\112\115")x.unstable_advanceTime(100)x.unstable_advanceTime(1000);(Z(x)).toFlushWithoutYielding();(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\095\095\095\095\095\095\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\n\084\097\115\107 \049 \091\078\111\114\109\097\108\093              \226\148\130\088\088\088\088\088\088\079 \101\114\114\111\114\101\100\n")end)i("\109\097\114\107\115 \119\104\101\110 \109\117\108\116\105\112\108\101 \116\097\115\107\115 \097\114\101 \099\097\110\099\101\108\101\100",function()x.unstable_Profiling.startLoggingProfilingEvents()local U=m(b,function()x.unstable_yieldValue("\089\105\101\108\100 \049")x.unstable_advanceTime(300)x.unstable_yieldValue("\089\105\101\108\100 \050")return function()x.unstable_yieldValue("\067\111\110\116\105\110\117\097\116\105\111\110")x.unstable_advanceTime(200)end end)local S=m(b,function()x.unstable_yieldValue("\089\105\101\108\100 \051")x.unstable_advanceTime(300)x.unstable_yieldValue("\089\105\101\108\100 \052")return function()x.unstable_yieldValue("\067\111\110\116\105\110\117\097\116\105\111\110")x.unstable_advanceTime(200)end end);(Z(x)).toFlushAndYieldThrough({"\089\105\101\108\100 \049","\089\105\101\108\100 \050"})x.unstable_advanceTime(100)A(U)A(S)x.unstable_advanceTime(1000);(Z(x)).toFlushWithoutYielding();(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\095\095\095\095\095\095\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\n\084\097\115\107 \049 \091\078\111\114\109\097\108\093              \226\148\130\088\088\088\088\088\088\095\095\079 \099\097\110\099\101\108\101\100\n\084\097\115\107 \050 \091\078\111\114\109\097\108\093              \226\148\130\095\095\095\095\095\095\095\095\079 \099\097\110\099\101\108\101\100\n")end)i("\104\097\110\100\108\101\115 \099\097\110\099\101\108\108\105\110\103 \097 \116\097\115\107\095 \116\104\097\116 \097\108\114\101\097\100\121 \102\105\110\105\115\104\101\100",function()x.unstable_Profiling.startLoggingProfilingEvents()local U=m(b,function()x.unstable_yieldValue("\065")x.unstable_advanceTime(1000)end);(Z(x)).toFlushAndYield({"\065"})A(U);(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\n\084\097\115\107 \049 \091\078\111\114\109\097\108\093              \226\148\130\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\n")end)i("\104\097\110\100\108\101\115 \099\097\110\099\101\108\108\105\110\103 \097 \116\097\115\107 \109\117\108\116\105\112\108\101 \116\105\109\101\115",function()x.unstable_Profiling.startLoggingProfilingEvents()m(b,function()x.unstable_yieldValue("\065")x.unstable_advanceTime(1000)end,{label="\065"})x.unstable_advanceTime(200)local U=m(b,function()x.unstable_yieldValue("\066")x.unstable_advanceTime(1000)end,{label="\066"})x.unstable_advanceTime(400)A(U)A(U)A(U);(Z(x)).toFlushAndYield({"\065"});(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\088\088\088\088\088\088\088\088\088\088\088\088\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\n\084\097\115\107 \049 \091\078\111\114\109\097\108\093              \226\148\130\095\095\095\095\095\095\095\095\095\095\095\095\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\n\084\097\115\107 \050 \091\078\111\114\109\097\108\093              \226\148\130    \095\095\095\095\095\095\095\095\079 \099\097\110\099\101\108\101\100\n")end)i("\104\097\110\100\108\101\115 \100\101\108\097\121\101\100 \116\097\115\107\115",function()x.unstable_Profiling.startLoggingProfilingEvents()m(b,function()x.unstable_advanceTime(1000)x.unstable_yieldValue("\065")end,{delay=1000});(Z(x)).toFlushWithoutYielding()x.unstable_advanceTime(1000);(Z(x)).toFlushAndYield({"\065"});(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\n\084\097\115\107 \049 \091\078\111\114\109\097\108\093              \226\148\130                    \088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\n")end)i("\104\097\110\100\108\101\115 \099\097\110\099\101\108\108\105\110\103 \097 \100\101\108\097\121\101\100 \084\097\115\107",function()x.unstable_Profiling.startLoggingProfilingEvents()local U=m(b,function()return x.unstable_yieldValue("\065")end,{delay=1000})A(U);(Z(x)).toFlushWithoutYielding();(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\n")end)i("\097\117\116\111\109\097\116\105\099\097\108\108\121 \115\116\111\112\115 \112\114\111\102\105\108\105\110\103 \097\110\100 \119\097\114\110\115 \105\102 \101\118\101\110\116 \108\111\103 \103\101\116\115 \116\111\111 \098\105\103",function()x.unstable_Profiling.startLoggingProfilingEvents()local U=41000 local S=1;(Z(function()while S<U do S+=1 local U=m(b,function()return{}end)A(U);(Z(x)).toFlushAndYield({})end end)).toErrorDev("\069\118\101\110\116 \108\111\103 \101\120\099\101\101\100\101\100 \109\097\120\105\109\117\109 \115\105\122\101",{withoutStack=true});(Z(Y())).toEqual("\040\101\109\112\116\121 \112\114\111\102\105\108\101\041")x.unstable_Profiling.startLoggingProfilingEvents()m(b,function()x.unstable_advanceTime(1000)end);(Z(x)).toFlushAndYield({});(Z(Y())).toEqual("\n\033\033\033 \077\097\105\110 \116\104\114\101\097\100              \226\148\130\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\095\n\084\097\115\107 \052\049\048\048\048 \091\078\111\114\109\097\108\093          \226\148\130\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\088\n")end)end)
-- ROBLOX upstream: https://github.com/facebook/react/blob/8af27aeedbc6b00bc2ef49729fc84f116c70a27c/packages/scheduler/src/__tests__/SchedulerProfiling-test.js
--[[**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 *
 * LICENSE file in the root directory of this source tree.
 * @flow
 *]]

local JestGlobals = require("@pkg/@jsdotlua/jest-globals")
local beforeEach = JestGlobals.beforeEach
local jestExpect = JestGlobals.expect
local describe = JestGlobals.describe
local it = JestGlobals.it
local jest = JestGlobals.jest
-- ROBLOX note: this uses a post-17.0.1 commit that removes a reliance on SharedArrayBuffer, but remains API compatible with 17.x

local Scheduler
local ImmediatePriority
local UserBlockingPriority
local NormalPriority
local LowPriority
local IdlePriority
local scheduleCallback
local cancelCallback
local function priorityLevelToString(priorityLevel)
	if priorityLevel == ImmediatePriority then
		return "Immediate"
	elseif priorityLevel == UserBlockingPriority then
		return "User-blocking"
	elseif priorityLevel == NormalPriority then
		return "Normal"
	elseif priorityLevel == LowPriority then
		return "Low"
	elseif priorityLevel == IdlePriority then
		return "Idle"
	else
		return nil
	end
end
describe("Scheduler", function()
	it("profiling APIs are not available", function()
		local SchedulerFeatureFlags = require("../SchedulerFeatureFlags")
		SchedulerFeatureFlags.enableProfiling = false

		Scheduler = require("../Scheduler")()
		jestExpect(Scheduler.unstable_Profiling).toBe(nil)
	end)
	beforeEach(function()
		jest.resetModules()

		jest.useFakeTimers()
		local SchedulerFeatureFlags = require("../SchedulerFeatureFlags")
		SchedulerFeatureFlags.enableProfiling = true

		-- ROBLOX deviation: In react, jest mocks Scheduler -> unstable_mock since
		-- unstable_mock depends on the real Scheduler, and our mock
		-- functionality isn't smart enough to prevent self-requires, we simply
		-- require the mock entry point directly for use in tests
		Scheduler = require("../unstable_mock")
		ImmediatePriority = Scheduler.unstable_ImmediatePriority
		UserBlockingPriority = Scheduler.unstable_UserBlockingPriority
		NormalPriority = Scheduler.unstable_NormalPriority
		LowPriority = Scheduler.unstable_LowPriority
		IdlePriority = Scheduler.unstable_IdlePriority
		scheduleCallback = Scheduler.unstable_scheduleCallback
		cancelCallback = Scheduler.unstable_cancelCallback
	end)
	local TaskStartEvent = 1
	local TaskCompleteEvent = 2
	local TaskErrorEvent = 3
	local TaskCancelEvent = 4
	local TaskRunEvent = 5
	local TaskYieldEvent = 6
	local SchedulerSuspendEvent = 7
	local SchedulerResumeEvent = 8
	local function stopProfilingAndPrintFlamegraph()
		local eventBuffer = Scheduler.unstable_Profiling.stopLoggingProfilingEvents()
		if eventBuffer == nil then
			return "(empty profile)"
		end
		local eventLog = { table.unpack(eventBuffer) }
		local tasks = {}
		local mainThreadRuns = {}
		local isSuspended = true
		local i = 1
		while i <= #eventLog do
			local instruction = eventLog[i][1]
			local time_ = eventLog[i][2]
			if instruction == 0 then
				break
			elseif instruction == TaskStartEvent then
				local taskId = eventLog[i][3]
				local priorityLevel = eventLog[i][4]
				local task_ = {
					id = taskId,
					priorityLevel = priorityLevel,
					label = nil,
					start = time_,
					end_ = -1,
					exitStatus = nil,
					runs = {},
				}
				tasks[taskId] = task_
				i += 1
			elseif instruction == TaskCompleteEvent then
				if isSuspended then
					error("Task cannot Complete outside the work loop.")
				end
				local taskId = eventLog[i][3]
				local task_ = tasks[taskId]
				if task_ == nil then
					error("Task does not exist.")
				end
				task_.end_ = time_
				task_.exitStatus = "completed"
				i += 1
			elseif instruction == TaskErrorEvent then
				if isSuspended then
					error("Task cannot Error outside the work loop.")
				end
				local taskId = eventLog[i][3]
				local task_ = tasks[taskId]
				if task_ == nil then
					error("Task does not exist.")
				end
				task_.end_ = time_
				task_.exitStatus = "errored"
				i += 1
			elseif instruction == TaskCancelEvent then
				local taskId = eventLog[i][3]
				local task_ = tasks[taskId]
				if task_ == nil then
					error("Task does not exist.")
				end
				task_.end_ = time_
				task_.exitStatus = "canceled"
				i += 1
			elseif instruction == TaskRunEvent or instruction == TaskYieldEvent then
				if isSuspended then
					error("Task cannot Run or Yield outside the work loop.")
				end
				local taskId = eventLog[i][3]
				local task_ = tasks[taskId]
				if task_ == nil then
					error("Task does not exist.")
				end
				table.insert(task_.runs, time_)
				i += 1
			elseif instruction == SchedulerSuspendEvent then
				if isSuspended then
					error("Scheduler cannot Suspend outside the work loop.")
				end
				isSuspended = true
				table.insert(mainThreadRuns, time_)
				i += 1
			elseif instruction == SchedulerResumeEvent then
				if not isSuspended then
					error("Scheduler cannot Resume inside the work loop.")
				end
				isSuspended = false
				table.insert(mainThreadRuns, time_)
				i += 1
			else
				error("Unknown instruction type: " + instruction)
			end
		end
		local labelColumnWidth = 30
		local microsecondsPerChar = 50000
		local result = ""
		local mainThreadLabelColumn = "!!! Main thread              "
		local mainThreadTimelineColumn = ""
		local isMainThreadBusy = true
		for _, time_ in mainThreadRuns do
			local index = time_ / microsecondsPerChar
			for i = 1, index - string.len(mainThreadTimelineColumn), 1 do
				mainThreadTimelineColumn ..= (function()
					if isMainThreadBusy then
						return "X"
					end
					return "_"
				end)()
			end
			isMainThreadBusy = not isMainThreadBusy
		end
		result ..= mainThreadLabelColumn .. "│" .. mainThreadTimelineColumn .. "\n"
		local tasksValues = {}
		for _, tasksValue in tasks do
			table.insert(tasksValues, tasksValue)
		end
		table.sort(tasksValues, function(t1, t2)
			return t2.priorityLevel > t1.priorityLevel
		end)
		for _, task_ in tasksValues do
			local label = task_.label
			if label == nil then
				label = "Task"
			end
			local labelColumn = string.format(
				"Task %d [%s]",
				task_.id,
				priorityLevelToString(task_.priorityLevel)
			)
			for i = 1, labelColumnWidth - string.len(labelColumn) - 1, 1 do
				labelColumn ..= " "
			end

			-- Add empty space up until the start mark
			local timelineColumn = ""
			for i = 1, task_.start / microsecondsPerChar, 1 do
				timelineColumn ..= " "
			end

			local isRunning = false
			for _, time_ in task_.runs do
				local index = time_ / microsecondsPerChar
				for i = 1, index - string.len(timelineColumn), 1 do
					timelineColumn ..= (function()
						if isRunning then
							return "X"
						end
						return "_"
					end)()
				end

				isRunning = not isRunning
			end

			local endIndex = task_.end_ / microsecondsPerChar
			for i = 1, endIndex - string.len(timelineColumn), 1 do
				timelineColumn ..= (function()
					if isRunning then
						return "X"
					end
					return "_"
				end)()
			end

			if task_.exitStatus ~= "completed" then
				timelineColumn ..= "O " .. (task_.exitStatus or "")
			end

			result ..= labelColumn .. "│" .. timelineColumn .. "\n"
		end
		return "\n" .. result
	end

	it("creates a basic flamegraph", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		Scheduler.unstable_advanceTime(100)
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_advanceTime(300)
			Scheduler.unstable_yieldValue("Yield 1")
			scheduleCallback(UserBlockingPriority, function()
				Scheduler.unstable_yieldValue("Yield 2")
				Scheduler.unstable_advanceTime(300)
			end, {
				label = "Bar",
			})
			Scheduler.unstable_advanceTime(100)
			Scheduler.unstable_yieldValue("Yield 3")
			return function()
				Scheduler.unstable_yieldValue("Yield 4")
				Scheduler.unstable_advanceTime(300)
			end
		end, {
			label = "Foo",
		})
		jestExpect(Scheduler).toFlushAndYieldThrough({ "Yield 1", "Yield 3" })
		Scheduler.unstable_advanceTime(100)
		jestExpect(Scheduler).toFlushAndYield({ "Yield 2", "Yield 4" })
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │XX________XX____________
Task 2 [User-blocking]       │        ____XXXXXX
Task 1 [Normal]              │  XXXXXXXX________XXXXXX
]])
	end)
	it("marks when a Task is canceled", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		local task_ = scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("Yield 1")
			Scheduler.unstable_advanceTime(300)
			Scheduler.unstable_yieldValue("Yield 2")
			return function()
				Scheduler.unstable_yieldValue("Continuation")
				Scheduler.unstable_advanceTime(200)
			end
		end)
		jestExpect(Scheduler).toFlushAndYieldThrough({ "Yield 1", "Yield 2" })
		Scheduler.unstable_advanceTime(100)
		cancelCallback(task_)
		Scheduler.unstable_advanceTime(1000)
		jestExpect(Scheduler).toFlushWithoutYielding()
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │______XXXXXXXXXXXXXXXXXXXXXX
Task 1 [Normal]              │XXXXXX__O canceled
]])
	end)
	it("marks when a task errors", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_advanceTime(300)
			error("Oops")
		end)
		jestExpect(Scheduler).toFlushAndThrow("Oops")
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_advanceTime(1000)
		jestExpect(Scheduler).toFlushWithoutYielding()
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │______XXXXXXXXXXXXXXXXXXXXXX
Task 1 [Normal]              │XXXXXXO errored
]])
	end)

	it("marks when multiple tasks are canceled", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		local task1 = scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("Yield 1")
			Scheduler.unstable_advanceTime(300)
			Scheduler.unstable_yieldValue("Yield 2")
			return function()
				Scheduler.unstable_yieldValue("Continuation")
				Scheduler.unstable_advanceTime(200)
			end
		end)
		local task2 = scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("Yield 3")
			Scheduler.unstable_advanceTime(300)
			Scheduler.unstable_yieldValue("Yield 4")
			return function()
				Scheduler.unstable_yieldValue("Continuation")
				Scheduler.unstable_advanceTime(200)
			end
		end)
		jestExpect(Scheduler).toFlushAndYieldThrough({ "Yield 1", "Yield 2" })
		Scheduler.unstable_advanceTime(100)
		cancelCallback(task1)
		cancelCallback(task2)
		Scheduler.unstable_advanceTime(1000)
		jestExpect(Scheduler).toFlushWithoutYielding()
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │______XXXXXXXXXXXXXXXXXXXXXX
Task 1 [Normal]              │XXXXXX__O canceled
Task 2 [Normal]              │________O canceled
]])
	end)
	it("handles cancelling a task_ that already finished", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		local task_ = scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("A")
			Scheduler.unstable_advanceTime(1000)
		end)
		jestExpect(Scheduler).toFlushAndYield({ "A" })
		cancelCallback(task_)
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │____________________
Task 1 [Normal]              │XXXXXXXXXXXXXXXXXXXX
]])
	end)

	it("handles cancelling a task multiple times", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("A")
			Scheduler.unstable_advanceTime(1000)
		end, {
			label = "A",
		})
		Scheduler.unstable_advanceTime(200)
		local task_ = scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("B")
			Scheduler.unstable_advanceTime(1000)
		end, {
			label = "B",
		})
		Scheduler.unstable_advanceTime(400)
		cancelCallback(task_)
		cancelCallback(task_)
		cancelCallback(task_)
		jestExpect(Scheduler).toFlushAndYield({ "A" })
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │XXXXXXXXXXXX____________________
Task 1 [Normal]              │____________XXXXXXXXXXXXXXXXXXXX
Task 2 [Normal]              │    ________O canceled
]])
	end)
	it("handles delayed tasks", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_advanceTime(1000)
			Scheduler.unstable_yieldValue("A")
		end, {
			delay = 1000,
		})
		jestExpect(Scheduler).toFlushWithoutYielding()
		Scheduler.unstable_advanceTime(1000)
		jestExpect(Scheduler).toFlushAndYield({ "A" })
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │XXXXXXXXXXXXXXXXXXXX____________________
Task 1 [Normal]              │                    XXXXXXXXXXXXXXXXXXXX
]])
	end)
	it("handles cancelling a delayed Task", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		local task_ = scheduleCallback(NormalPriority, function()
			return Scheduler.unstable_yieldValue("A")
		end, {
			delay = 1000,
		})
		cancelCallback(task_)
		jestExpect(Scheduler).toFlushWithoutYielding()
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │
]])
	end)
	it("automatically stops profiling and warns if event log gets too big", function()
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		-- ROBLOX deviation: use toWarvDev matcher below instead of overriding console global
		-- spyOnDevAndProd(console, "error")
		-- ROBLOX deviation: any lower than this, and the buffer doesn't overslow and we try to table.unpack() too many elements
		local originalMaxIterations = 41000
		local taskId = 1
		jestExpect(function()
			while taskId < originalMaxIterations do
				taskId += 1
				local task_ = scheduleCallback(NormalPriority, function()
					return {}
				end)
				cancelCallback(task_)
				jestExpect(Scheduler).toFlushAndYield({})
			end
		end).toErrorDev("Event log exceeded maximum size", { withoutStack = true })
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual("(empty profile)")
		Scheduler.unstable_Profiling.startLoggingProfilingEvents()
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_advanceTime(1000)
		end)
		jestExpect(Scheduler).toFlushAndYield({})
		jestExpect(stopProfilingAndPrintFlamegraph()).toEqual([[

!!! Main thread              │____________________
Task 41000 [Normal]          │XXXXXXXXXXXXXXXXXXXX
]])
	end)
end)
