diff --git a/.buildkite/hierarchies/pipeline.yml b/.buildkite/hierarchies/pipeline.yml index 9755c22c95..2fceec1457 100644 --- a/.buildkite/hierarchies/pipeline.yml +++ b/.buildkite/hierarchies/pipeline.yml @@ -13,7 +13,6 @@ env: SLURM_KILL_BAD_EXIT: 1 CONFIG_PATH: "config/longrun_configs" - PERF_CONFIG_PATH: "config/perf_configs" timeout_in_minutes: 1440 diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 76a4a8de04..e5d17740e7 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -14,7 +14,6 @@ env: SLURM_KILL_BAD_EXIT: 1 CONFIG_PATH: "config/ci_configs" - PERF_CONFIG_PATH: "config/perf_configs" timeout_in_minutes: 240 @@ -42,8 +41,7 @@ steps: - echo "--- Instantiate ClimaEarth env" - "julia --project=experiments/ClimaEarth/ -e 'using Pkg; Pkg.develop(path=\".\")'" - "julia --project=experiments/ClimaEarth/ -e 'using Pkg; Pkg.instantiate(;verbose=true)'" - # ProfileCanvas and JSON are used by the perf jobs - - "julia --project=experiments/ClimaEarth/ -e 'using Pkg; Pkg.add(\"MPI\"); Pkg.add(\"CUDA\"); Pkg.add(\"ProfileCanvas\"); Pkg.add(\"JSON\")'" + - "julia --project=experiments/ClimaEarth/ -e 'using Pkg; Pkg.add(\"MPI\"); Pkg.add(\"CUDA\");'" - "julia --project=experiments/ClimaEarth/ -e 'using Pkg; Pkg.precompile()'" - "julia --project=experiments/ClimaEarth/ -e 'using Pkg; Pkg.status()'" @@ -98,12 +96,6 @@ steps: slurm_ntasks: 2 slurm_mem: 16GB - - label: "Perf flame graph diff tests" - command: "julia --color=yes --project=experiments/ClimaEarth/ perf/flame_test.jl --job_id flame_perf_target" - timeout_in_minutes: 5 - agents: - slurm_mem: 16GB - - group: "GPU: unit tests and global bucket" steps: - label: "GPU runtests" @@ -202,33 +194,6 @@ steps: # ... - - # PERFORMANCE - - # slabplanet default: track unthreaded performance (alloc tests, flame graph, flame graph diff, build history) - - label: ":rocket: Slabplanet: default (unthreaded)" - key: "slabplanet_unthreaded" - command: "julia --color=yes --project=experiments/ClimaEarth/ experiments/ClimaEarth/run_amip.jl --config_file $CONFIG_PATH/default_unthreaded.yml --job_id default_unthreaded" - artifact_paths: "experiments/ClimaEarth/output/slabplanet/default_unthreaded/artifacts/*" - env: - FLAME_PLOT: "" - BUILD_HISTORY_HANDLE: "" - agents: - slurm_ntasks: 1 - slurm_mem: 20GB - - - label: ":rocket: Slabplanet: default (unthreaded) - flame graph and allocation tests" - command: "julia --color=yes --project=experiments/ClimaEarth perf/flame.jl --config_file $PERF_CONFIG_PATH/perf_default_unthreaded.yml --job_id perf_default_unthreaded" - artifact_paths: "perf/output/perf_default_unthreaded/*" - agents: - slurm_mem: 20GB - - - label: ":rocket: Slabplanet: default (unthreaded) - flame graph diff" - command: "julia --color=yes --project=experiments/ClimaEarth perf/flame_diff.jl --config_file $PERF_CONFIG_PATH/perf_diff_default_unthreaded.yml --job_id perf_diff_default_unthreaded" - artifact_paths: "perf/output/perf_diff_default_unthreaded/*" - agents: - slurm_mem: 20GB - # < end Drivers for release # CLIMACORE EXPERIMENTS @@ -333,20 +298,6 @@ steps: agents: slurm_mem: 20GB - # PERFORMANCE RUNS: flame graphs + allocation tests - - - label: ":rocket: flame graph and allocation tests: perf_coarse_single_ft64" - command: "julia --color=yes --project=experiments/ClimaEarth perf/flame.jl --config_file $PERF_CONFIG_PATH/perf_coarse_single_ft64.yml --job_id perf_coarse_single_ft64" - artifact_paths: "perf/output/perf_coarse_single_ft64/*" - agents: - slurm_mem: 20GB - - - label: ":rocket: performance: flame graph diff: perf_diff_coarse_single_ft64" - command: "julia --color=yes --project=experiments/ClimaEarth perf/flame_diff.jl --config_file $PERF_CONFIG_PATH/perf_diff_coarse_single_ft64.yml --job_id perf_diff_coarse_single_ft64" - artifact_paths: "perf/output/perf_diff_coarse_single_ft64/*" - agents: - slurm_mem: 20GB - - group: "Hierarchy tests (1d)" steps: - label: ":construction: Dry Held Suarez" diff --git a/.dev/up_deps.jl b/.dev/up_deps.jl index 9c354ace36..e3bce0cb84 100644 --- a/.dev/up_deps.jl +++ b/.dev/up_deps.jl @@ -6,10 +6,8 @@ files in all of our environments. root = dirname(@__DIR__) dirs = ( root, - joinpath(root, "artifacts"), joinpath(root, "test"), joinpath(root, ".dev"), - joinpath(root, "perf"), joinpath(root, "docs"), joinpath(root, "experiments/ClimaEarth"), ) diff --git a/.gitignore b/.gitignore index 6ed11efb34..f301fa8f17 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ docs/src/generated/ # Experiments !experiments/ClimaEarth/**/Manifest.toml !experiments/ClimaCore/**/Manifest.toml -!perf/Manifest.toml # Output output/ diff --git a/docs/src/performance.md b/docs/src/performance.md index d2cc1aa1ff..400a694c9e 100644 --- a/docs/src/performance.md +++ b/docs/src/performance.md @@ -1,5 +1,11 @@ # Performance Analysis Tools +Until commit +[7a7e98](https://github.com/CliMA/ClimaCoupler.jl/tree/7a7e98db25fe740b6ff0e3eca1e028e876a8a09e), +ClimaCoupler provided included performance jobs. You can find them by following the link above. + +Below is a short description. + `ClimaCoupler.jl` provides basic tools for analyzing performance: 1. **Flame graphs**: the `perf/flame.jl` script is run by Buildkite to produce flame graphs using [ProfileCanvas.jl](https://github.com/pfitzseb/ProfileCanvas.jl) in the `perf/output/` directory. 2. **Job walltime and allocation history**: use Buildkite to trigger the [`build_history`](https://github.com/CliMA/slurm-buildkite/blob/master/bin/build_history) script to output an interactive plot with the history of memory usage and time elapsed for each tracked job (default: current build and past builds of the `staging` branch over the past year). Use `key` to select which jobs to track. More documentation can be found in the [SLURM-Buildkite Wiki](https://github.com/CliMA/slurm-buildkite/wiki/Memory#plotting-memory-usage-over-time). diff --git a/perf/ProfileCanvasDiff.jl b/perf/ProfileCanvasDiff.jl deleted file mode 100644 index a7ebcc7bd6..0000000000 --- a/perf/ProfileCanvasDiff.jl +++ /dev/null @@ -1,492 +0,0 @@ -# temporarily copied and modified from https://github.com/pfitzseb/ProfileCanvas.jl - -module ProfileCanvasDiff - -using Profile, JSON, REPL, Pkg.Artifacts, Base64 - -export @profview, @profview_allocs - -struct ProfileData - data::Any - typ::Any -end - -mutable struct ProfileFrame - func::String - file::String # human readable file name - path::String # absolute path - line::Int # 1-based line number - count::Int # number of samples in this frame - countLabel::Union{Missing, String} # defaults to `$count samples` - flags::UInt8 # any or all of ProfileFrameFlag - taskId::Union{Missing, UInt} - children::Vector{ProfileFrame} - count_change::Float64 # change in count or self_count: (new - old) -end - -struct ProfileDisplay <: Base.Multimedia.AbstractDisplay end - -function __init__() - pushdisplay(ProfileDisplay()) - - atreplinit(i -> begin - while ProfileDisplay() in Base.Multimedia.displays - popdisplay(ProfileDisplay()) - end - pushdisplay(ProfileDisplay()) - end) -end - -function jlprofile_data_uri(build_path) - path = joinpath(build_path, "ProfileViewerDiff.js") - @info "the module path is $path" - str = read(path, String) - - return string("data:text/javascript;base64,", base64encode(str)) -end - -function Base.show(io::IO, ::MIME"text/html", canvas::ProfileData, build_path = "") - id = "profiler-container-$(round(Int, rand()*100000))" - - data_uri = (jlprofile_data_uri(build_path)) - println( - io, - """ -
- - """, - ) -end - -function Base.display(_::ProfileDisplay, canvas::ProfileData) - - file = html_file(string(tempname(), ".html"), canvas) - url = "file://$file" - - if Sys.iswindows() - run(`cmd /c "start $url"`) - elseif Sys.isapple() - run(`open $url`) - elseif Sys.islinux() || Sys.isbsd() - run(`xdg-open $url`) - end -end - -html_file( - filename, - data = Profile.fetch(); - build_path = "", - tracked_list = Dict{String, Int}(;), - self_count = false, - kwargs..., -) = html_file( - filename, - build_path = build_path, - view(data; tracked_list = tracked_list, self_count = self_count, kwargs...)[1], -) - -function html_file(file::AbstractString, canvas::ProfileData; build_path = "") - @assert endswith(file, ".html") - - data_uri = jlprofile_data_uri(build_path) - open(file, "w") do io - id = "profiler-container-$(round(Int, rand()*100000))" - - println( - io, - """ - - - - - - - - - - """, - ) - end - return file -end - -using Profile - -# https://github.com/timholy/FlameGraphs.jl/blob/master/src/graph.jl -const ProfileFrameFlag = ( - RuntimeDispatch = UInt8(2^0), - GCEvent = UInt8(2^1), - REPL = UInt8(2^2), - Compilation = UInt8(2^3), - TaskEvent = UInt8(2^4), -) - -function view(data = Profile.fetch(); C = false, tracked_list = Dict{String, Int}(;), self_count = false, kwargs...) - - d = Dict{String, ProfileFrame}() - - new_tracked_list = Dict{String, Int}(;) - - if VERSION >= v"1.8.0-DEV.460" - threads = ["all", 1:Threads.nthreads()...] - else - threads = ["all"] - end - - if isempty(data) - Profile.warning_empty() - return - end - - lidict = Profile.getdict(unique(data)) - data_u64 = convert(Vector{UInt64}, data) - for thread in threads - graph = stackframetree(data_u64, lidict; thread = thread, kwargs...) - curr_tree = make_tree( - ProfileFrame("root", "", "", 0, graph.count, missing, 0x0, missing, ProfileFrame[], 999), #root process - graph; - C = C, - tracked_list = tracked_list, - kwargs..., - ) - - # if self_count, convert counts (current function + children) to self_counts (current function only) - curr_tree = self_count ? iterate_self_change!(curr_tree, tracked_list) : curr_tree - - # create a new Dict with the counts of the current job - new_tracked_list = log_counts(curr_tree) - - d[string(thread)] = curr_tree - - end - - - return (ProfileData(d, "Thread"), new_tracked_list) -end - -""" - iterate_self_change!(flame_tree::Main.ProfileCanvasDiff.ProfileFrame, tracked_list::Dict) - -Iterates over all child nodes of the flame_tree, calling `collect_self_change!`. -""" -function iterate_self_change!(flame_tree::Main.ProfileCanvasDiff.ProfileFrame, tracked_list::Dict) - - if isempty(flame_tree.children) - child_sum = 0 - collect_self_change!(flame_tree, child_sum, tracked_list) - else - child_sum = sum(map(x -> x.count, flame_tree.children)) - collect_self_change!(flame_tree, child_sum, tracked_list) - for sf in flame_tree.children - iterate_self_change!(sf, tracked_list) - - end - end - return flame_tree -end - -""" - collect_self_change!(flame_tree::Main.ProfileCanvasDiff.ProfileFrame, child_sum::Int, tracked_list::Dict) - -Collects information on the change in allocation by individual functions -(minus the allocation of their children) between the current and the reference jobs. -""" -function collect_self_change!(flame_tree::Main.ProfileCanvasDiff.ProfileFrame, child_sum::Int, tracked_list::Dict) - func = string(flame_tree.func) - line = string(flame_tree.line) - file = string(flame_tree.file) - - func_sign = "self_count_$func.$file.$line" - current_self_count = flame_tree.count - child_sum - old_self_count = func_sign in keys(tracked_list) ? tracked_list[func_sign] : -999 * flame_tree.count # if not found, this will be marked untracked - - overall_count_change = deepcopy(flame_tree.count_change) - flame_tree.count_change = (current_self_count - old_self_count) - - (overall_count_change, old_self_count) = - old_self_count < 0 ? ("untracked", "untracked") : (overall_count_change, old_self_count) - - flame_tree.countLabel = - string(flame_tree.count) * - " samples \n ∑child_count = $child_sum counts \n Δcount = $overall_count_change counts \n Δself_count = $current_self_count - $old_self_count =" * - string(flame_tree.count_change) * - " counts \n " -end - -""" - log_counts(flame_tree::Main.ProfileCanvasDiff.ProfileFrame, ct = 0, dict = Dict{String, Float64}()) - -Iterate over all children of a stack tree and save their names ("\$func.\$file.\$line") and -corresponding count values in a Dict. -""" -function log_counts(flame_tree::Main.ProfileCanvasDiff.ProfileFrame, ct = 0, dict = Dict{String, Float64}()) - ct += 1 - line = flame_tree.line - file = flame_tree.file - func = flame_tree.func - push!(dict, "$func.$file.$line" => flame_tree.count) - - if isempty(flame_tree.children) - # 0 upstream contribution if end child - push!(dict, "self_count_$func.$file.$line" => flame_tree.count) - - else - child_sum = sum(map(x -> x.count, flame_tree.children)) - push!(dict, "self_count_$func.$file.$line" => flame_tree.count - child_sum) - for sf in flame_tree.children - log_counts(sf, ct, dict) - end - end - return dict -end - -function stackframetree(data_u64, lidict; thread = nothing, combine = true, recur = :off) - root = combine ? Profile.StackFrameTree{StackTraces.StackFrame}() : Profile.StackFrameTree{UInt64}() - if VERSION >= v"1.8.0-DEV.460" - thread = thread == "all" ? (1:Threads.nthreads()) : thread - root, _ = Profile.tree!(root, data_u64, lidict, true, recur, thread) - else - root = Profile.tree!(root, data_u64, lidict, true, recur) - end - if !isempty(root.down) - root.count = sum(pr -> pr.second.count, root.down) - end - - return root -end - -function status(sf::StackTraces.StackFrame) - st = UInt8(0) - if sf.from_c && (sf.func === :jl_invoke || sf.func === :jl_apply_generic || sf.func === :ijl_apply_generic) - st |= ProfileFrameFlag.RuntimeDispatch - end - if sf.from_c && startswith(String(sf.func), "jl_gc_") - st |= ProfileFrameFlag.GCEvent - end - if !sf.from_c && sf.func === :eval_user_input && endswith(String(sf.file), "REPL.jl") - st |= ProfileFrameFlag.REPL - end - if !sf.from_c && occursin("./compiler/", String(sf.file)) - st |= ProfileFrameFlag.Compilation - end - if !sf.from_c && occursin("task.jl", String(sf.file)) - st |= ProfileFrameFlag.TaskEvent - end - return st -end - -function status(node::Profile.StackFrameTree, C::Bool) - st = status(node.frame) - C && return st - # If we're suppressing C frames, check all C-frame children - for child in values(node.down) - child.frame.from_c || continue - st |= status(child, C) - end - return st -end - -function add_child(graph::ProfileFrame, node, C::Bool; tracked_list = Dict{String, Int}(;)) - name = string(node.frame.file) - func = string(node.frame.func) - line = string(node.frame.line) - file = basename(string(node.frame.file)) - func_sign = "$func.$file.$line" - - if func == "" - func = "unknown" - end - - old_count = func_sign in keys(tracked_list) ? tracked_list[func_sign] : 999 - current_count = Float64(node.count) - - frame = ProfileFrame( - func, - basename(name), - name, - node.frame.line, - node.count, - missing, - status(node, C), - missing, - ProfileFrame[], - current_count - Float64(old_count), - ) - - push!(graph.children, frame) - - return frame -end - -function make_tree(graph, node::Profile.StackFrameTree; C = false, tracked_list = Dict{String, Int}(;)) - for child_node in sort!(collect(values(node.down)); rev = true, by = node -> node.count) - # child not a hidden frame - if C || !child_node.frame.from_c - child = add_child(graph, child_node, C, tracked_list = tracked_list) - make_tree(child, child_node; C = C, tracked_list = tracked_list) - else - make_tree(graph, child_node, tracked_list = tracked_list) - end - end - - return graph -end - -""" - @profview f(args...) [C = false] - -Clear the Profile buffer, profile `f(args...)`, and view the result graphically. - -The default of `C = false` will only show Julia frames in the profile graph. -""" -macro profview(ex, args...) - return quote - Profile.clear() - Profile.@profile $(esc(ex)) - view(; $(esc.(args)...)) - end -end - -## Allocs - -""" - @profview_allocs f(args...) [sample_rate=0.0001] [C=false] - -Clear the Profile buffer, profile `f(args...)`, and view the result graphically. -""" -macro profview_allocs(ex, args...) - sample_rate_expr = :(sample_rate = 0.0001) - for arg in args - if Meta.isexpr(arg, :(=)) && length(arg.args) > 0 && arg.args[1] === :sample_rate - sample_rate_expr = arg - end - end - if isdefined(Profile, :Allocs) - return quote - Profile.Allocs.clear() - Profile.Allocs.@profile $(esc(sample_rate_expr)) $(esc(ex)) - view_allocs() - end - else - return :(@error "This version of Julia does not support the allocation profiler.") - end -end - -function view_allocs(_results = Profile.Allocs.fetch(); C = false) - results = _results::Profile.Allocs.AllocResults - allocs = results.allocs - - allocs_root = ProfileFrame("root", "", "", 0, 0, missing, 0x0, missing, ProfileFrame[], 0) - counts_root = ProfileFrame("root", "", "", 0, 0, missing, 0x0, missing, ProfileFrame[], 0) - for alloc in allocs - this_allocs = allocs_root - this_counts = counts_root - - for sf in Iterators.reverse(alloc.stacktrace) - if !C && sf.from_c - continue - end - file = string(sf.file) - this_counts′ = ProfileFrame( - string(sf.func), - basename(file), - file, - sf.line, - 0, - missing, - 0x0, - missing, - ProfileFrame[], - 0, - ) - ind = findfirst( - c -> (c.func == this_counts′.func && c.path == this_counts′.path && c.line == this_counts′.line), - this_allocs.children, - ) - - this_counts, this_allocs = if ind === nothing - push!(this_counts.children, this_counts′) - this_allocs′ = deepcopy(this_counts′) - push!(this_allocs.children, this_allocs′) - - (this_counts′, this_allocs′) - else - (this_counts.children[ind], this_allocs.children[ind]) - end - this_allocs.count += alloc.size - this_allocs.countLabel = memory_size(this_allocs.count) - this_counts.count += 1 - this_allocs.count_change = 0.6 - end - - alloc_type = replace(string(alloc.type), "Profile.Allocs." => "") - ind = findfirst(c -> (c.func == alloc_type), this_allocs.children) - if ind === nothing - push!( - this_allocs.children, - ProfileFrame( - alloc_type, - "", - "", - 0, - this_allocs.count, - memory_size(this_allocs.count), - ProfileFrameFlag.GCEvent, - missing, - ProfileFrame[], - 0.6, - ), - ) - push!( - this_counts.children, - ProfileFrame(alloc_type, "", "", 0, 1, missing, ProfileFrameFlag.GCEvent, missing, ProfileFrame[], 1), - ) - else - this_counts.children[ind].count += 1 - this_allocs.children[ind].count += alloc.size - this_allocs.children[ind].countLabel = memory_size(this_allocs.children[ind].count) - this_allocs.children[ind].count_change = 0.6 - end - - counts_root.count += 1 - allocs_root.count += alloc.size - allocs_root.countLabel = memory_size(allocs_root.count) - allocs_root.count_change = 0.6 - end - - d = Dict{String, ProfileFrame}("size" => allocs_root, "count" => counts_root) - - return ProfileData(d, "Allocation") -end - -const prefixes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] -function memory_size(size) - i = 1 - while size > 1000 && i + 1 < length(prefixes) - size /= 1000 - i += 1 - end - return string(round(Int, size), " ", prefixes[i]) -end - - -end diff --git a/perf/ProfileViewerDiff.js b/perf/ProfileViewerDiff.js deleted file mode 100644 index b7464a6973..0000000000 --- a/perf/ProfileViewerDiff.js +++ /dev/null @@ -1,699 +0,0 @@ -/* temporarily copied and modified from https://github.com/pfitzseb/jl-profile.js/blob/a13f2ef7852bc1782ec180e3efd4ccef2da7ba6d/dist/profile-viewer.js */ -export class ProfileViewer { - constructor(element, data, selectorLabel) { - this.selections = []; - this.offsetX = 0; - this.offsetY = 0; - this.isWheeling = false; - this.canWheelDown = true; - this.scrollPosition = 0; - this.isResizing = false; - this.isDocumentScrolling = false; - this.isMouseMove = false; - this.scale = window.devicePixelRatio; - this.borderWidth = 2; - this.padding = 2; - this.fontConfig = '10px sans-serif'; - this.borderColor = '#fff'; - this.selectorLabel = 'Thread'; - this.boxHeight = 24; - this.destroyed = false; - if (typeof element === 'string') { - element = document.querySelector(element); - } - if (!element) { - throw new Error('Invalid parent element specified.'); - } - this.container = element; - if (selectorLabel) { - this.selectorLabel = selectorLabel; - } - this.insertDOM(); - this.getStyles(); - this.registerResizeObserver(); - this.registerScrollListener(); - if (data) { - this.setData(data); - } - this.getOffset(); - } - /** - * Remove event listeners and added child elements. The global stylesheet - * is only removed if this is the last reference to it (i.e. there are no - * other not-destroyed ProfileViewer instances in the DOM). - */ - destroy() { - this.destroyed = true; - this.resizeObserver.disconnect(); - if (this.scrollHandler) { - document.removeEventListener('scroll', this.scrollHandler); - } - if (this.stylesheet && parseInt(this.stylesheet.dataset.references) === 0) { - document.head.removeChild(this.stylesheet); - } - while (this.container.firstChild) { - this.container.removeChild(this.container.lastChild); - } - } - setData(data) { - if (this.destroyed) { - console.error('This profile viewer is destroyed.'); - return; - } - if (!data) { - this.data = data; - this.clear(); - return; - } - const selections = Object.keys(data); - selections.sort((a, b) => { - if (a === 'all') { - return -1; - } - if (b === 'all') { - return 1; - } - if (a < b) { - return -1; - } - if (a > b) { - return 1; - } - return 0; - }); - this.data = data; - this.selections = selections; - this.currentSelection = this.selections[0]; - this.activeNode = this.data[this.currentSelection]; - this.updateFilter(); - this.redraw(); - } - setSelectorLabel(label) { - this.selectorLabel = label; - this.selectorLabelElement.innerText = `${label}: `; - } - registerCtrlClickHandler(f) { - this.ctrlClickHandler = f; - } - /** - * @deprecated Use `registerSelectionHandler` instead. - */ - registerThreadSelectorHandler(f) { - this.selectionHandler = f; - } - registerSelectionHandler(f) { - this.selectionHandler = f; - } - registerScrollListener() { - document.addEventListener('scroll', this.scrollHandler); - } - clear() { - this.selections = []; - this.currentSelection = ''; - this.activeNode = undefined; - this.canvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); - this.hoverCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); - } - isDestroyed() { - return this.destroyed; - } - getStyles() { - var _a, _b, _c; - const style = window.getComputedStyle(this.container, null); - const fontFamily = style.fontFamily; - const fontSize = style.fontSize; - this.fontConfig = - parseInt(fontSize !== null && fontSize !== void 0 ? fontSize : '12px', 10) * this.scale + - 'px ' + - (fontFamily !== null && fontFamily !== void 0 ? fontFamily : 'sans-serif'); - this.borderColor = (_a = style.color) !== null && _a !== void 0 ? _a : '#000'; - this.canvasCtx.font = this.fontConfig; - this.canvasCtx.textBaseline = 'middle'; - const textMetrics = this.canvasCtx.measureText('ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]*\'"^_`abcdefghijklmnopqrstuvwxyz'); - this.boxHeight = Math.ceil((((_b = textMetrics.fontBoundingBoxDescent) !== null && _b !== void 0 ? _b : textMetrics.actualBoundingBoxDescent) + - ((_c = textMetrics.fontBoundingBoxAscent) !== null && _c !== void 0 ? _c : textMetrics.actualBoundingBoxAscent) + - 2 * this.borderWidth + - 2 * this.padding) * - this.scale); - if (this.activeNode) { - this.redraw(); - } - } - redraw() { - this.canWheelDown = false; - this.canvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); - this.clearHover(); - this.drawGraph(this.activeNode, this.canvasWidth, this.canvasHeight, 0, this.scrollPosition); - } - insertDOM() { - this.insertStylesheet(); - this.canvas = document.createElement('canvas'); - this.canvas.classList.add('__profiler-canvas'); - this.canvasCtx = this.canvas.getContext('2d'); - this.hoverCanvas = document.createElement('canvas'); - this.hoverCanvas.classList.add('__profiler-hover-canvas'); - this.hoverCanvasCtx = this.hoverCanvas.getContext('2d'); - const canvasContainer = document.createElement('div'); - canvasContainer.classList.add('__profiler-canvas-container'); - canvasContainer.appendChild(this.canvas); - canvasContainer.appendChild(this.hoverCanvas); - canvasContainer.appendChild(this.createTooltip()); - this.container.appendChild(this.createFilterContainer()); - this.container.appendChild(canvasContainer); - this.canvas.addEventListener('wheel', (ev) => { - if (!this.activeNode) { - return; - } - if (ev.deltaY > 0 && !this.canWheelDown) { - return; - } - if (ev.deltaY < 0 && this.scrollPosition === 0) { - if (-ev.deltaY > this.boxHeight) { - const parent = this.findParentNode(this.activeNode); - if (parent) { - ev.preventDefault(); - ev.stopPropagation(); - this.clearHover(); - this.activeNode = parent; - this.redraw(); - } - return; - } - } - ev.preventDefault(); - ev.stopPropagation(); - if (!this.isWheeling) { - window.requestAnimationFrame(() => { - this.scrollPosition = Math.min(0, this.scrollPosition - ev.deltaY); - this.redraw(); - this.isWheeling = false; - }); - this.isWheeling = true; - } - }); - this.canvas.addEventListener('mousemove', (ev) => { - if (!this.isMouseMove && this.activeNode) { - window.requestAnimationFrame(() => { - // XXX: this is bad - this.getOffset(); - const mouseX = ev.clientX - this.offsetX; - const mouseY = ev.clientY - this.offsetY; - this.hoverCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); - const didDraw = this.drawHover(this.activeNode, this.scale * mouseX, this.scale * mouseY); - if (didDraw) { - if (mouseX > this.canvasWidthCSS / 2) { - this.tooltipElement.style.right = - this.canvasWidthCSS - mouseX + 10 + 'px'; - this.tooltipElement.style.left = 'unset'; - } - else { - this.tooltipElement.style.right = 'unset'; - this.tooltipElement.style.left = mouseX + 10 + 'px'; - } - if (mouseY > this.canvasHeightCSS / 2) { - this.tooltipElement.style.bottom = - this.canvasHeightCSS - mouseY + 10 + 'px'; - this.tooltipElement.style.top = 'unset'; - } - else { - this.tooltipElement.style.bottom = 'unset'; - this.tooltipElement.style.top = mouseY + 10 + 'px'; - } - this.tooltipElement.style.display = 'block'; - } - else { - this.tooltipElement.style.display = 'none'; - } - this.isMouseMove = false; - }); - this.isMouseMove = true; - } - }); - this.canvas.addEventListener('click', (ev) => { - if (!this.activeNode) { - return; - } - ev.preventDefault(); - ev.stopPropagation(); - this.getOffset(); - const mouseX = this.scale * (ev.clientX - this.offsetX); - const mouseY = this.scale * (ev.clientY - this.offsetY); - if (ev.ctrlKey || ev.metaKey) { - this.runOnNodeAtMousePosition(this.activeNode, mouseX, mouseY, (node) => { - if (this.ctrlClickHandler) { - this.ctrlClickHandler(node); - } - }); - } - else { - if (this.zoomInOnNode(this.activeNode, mouseX, mouseY)) { - this.scrollPosition = 0; - this.redraw(); - } - else if (ev.detail === 2) { - // reset on double-click - this.resetView(); - } - } - }); - } - resetView() { - this.activeNode = this.data[this.currentSelection]; - this.scrollPosition = 0; - this.redraw(); - } - insertStylesheet() { - const stylesheet = document.querySelector('#__profiler_stylesheet'); - if (stylesheet) { - stylesheet.dataset.references = (parseInt(stylesheet.dataset.references) + 1).toString(); - this.stylesheet = stylesheet; - } - else { - this.stylesheet = document.createElement('style'); - this.stylesheet.setAttribute('id', '__profiler-stylesheet'); - this.stylesheet.dataset.references = '0'; - this.stylesheet.innerText = ` - .__profiler-canvas { - z-index: 0; - position: absolute; - width: 100%; - } - .__profiler-canvas-container { - width: 100%; - height: 100%; - position: relative; - } - .__profiler-hover-canvas { - z-index: 1; - position: absolute; - pointer-events: none; - width: 100%; - } - .__profiler-tooltip { - z-index: 2; - display: none; - position: absolute; - background-color: #ddd; - border: 1px solid black; - padding: 5px 10px; - pointer-events: none; - max-width: 45%; - overflow: hidden; - } - .__profiler-tooltip > div { - line-break: anywhere; - } - .__profiler-filter { - height: 30px; - padding: 2px 16px; - margin: 0; - box-sizing: border-box; - border-bottom: 1px solid #444; - user-select: none; - } - .__profiler-reset { - float: right; - } - `; - document.head.appendChild(this.stylesheet); - } - } - createTooltip() { - this.tooltipElement = document.createElement('div'); - this.tooltipElement.classList.add('__profiler-tooltip'); - this.tooltip = { - count: document.createElement('span'), - percentage: document.createElement('span'), - function: document.createElement('code'), - file: document.createElement('a'), - flags: document.createElement('span'), - }; - this.tooltip.function.classList.add('fname'); - const rows = [ - [this.tooltip.function], - [ - this.tooltip.count, - document.createTextNode(' ('), - this.tooltip.percentage, - document.createTextNode(') '), - ], - [this.tooltip.file], - [this.tooltip.flags], - ]; - for (const row of rows) { - const rowContainer = document.createElement('div'); - for (const col of row) { - rowContainer.appendChild(col); - } - this.tooltipElement.appendChild(rowContainer); - } - this.tooltip['ctrlClickHint'] = document.createElement('small'); - this.tooltipElement.appendChild(this.tooltip['ctrlClickHint']); - this.container.appendChild(this.tooltipElement); - return this.tooltipElement; - } - createFilterContainer() { - this.filterContainer = document.createElement('div'); - this.filterContainer.classList.add('__profiler-filter'); - this.selectorLabelElement = document.createElement('label'); - this.selectorLabelElement.innerText = `${this.selectorLabel}: `; - this.filterContainer.appendChild(this.selectorLabelElement); - this.filterInput = document.createElement('select'); - this.filterInput.addEventListener('change', () => { - this.currentSelection = this.filterInput.value; - if (this.selectionHandler) { - this.selectionHandler(this.currentSelection); - } - this.resetView(); - }); - this.filterContainer.appendChild(this.filterInput); - const resetter = document.createElement('button'); - resetter.classList.add('__profiler-reset'); - resetter.innerText = 'reset view'; - resetter.addEventListener('click', () => { - this.resetView(); - }); - this.filterContainer.appendChild(resetter); - return this.filterContainer; - } - updateFilter() { - while (this.filterInput.firstChild) { - this.filterInput.removeChild(this.filterInput.lastChild); - } - for (const selection of this.selections) { - const entry = document.createElement('option'); - entry.innerText = selection; - entry.setAttribute('value', selection); - this.filterInput.appendChild(entry); - } - } - registerResizeObserver() { - this.resizeObserver = new ResizeObserver((entries) => { - if (!this.isResizing) { - for (const entry of entries) { - if (entry.target === this.container) { - window.requestAnimationFrame(() => { - if (window.devicePixelRatio !== this.scale) { - this.scale = window.devicePixelRatio; - this.getStyles(); - } - this.canvasWidth = Math.round(entry.contentRect.width * this.scale); - this.canvasHeight = Math.round((entry.contentRect.height - 30) * this.scale); - this.canvasWidthCSS = entry.contentRect.width; - this.canvasHeightCSS = entry.contentRect.height; - this.canvas.width = this.canvasWidth; - this.canvas.height = this.canvasHeight; - this.hoverCanvas.width = this.canvasWidth; - this.hoverCanvas.height = this.canvasHeight; - this.redraw(); - this.isResizing = false; - }); - } - } - this.isResizing = true; - } - }); - this.resizeObserver.observe(this.container); - } - scrollHandler(e) { - if (!this.isDocumentScrolling) { - window.requestAnimationFrame(() => { - this.getOffset(); - this.isDocumentScrolling = false; - }); - this.isDocumentScrolling = true; - } - } - getOffset() { - const box = this.canvas.getBoundingClientRect(); - this.offsetX = box.left; - this.offsetY = box.top; - } - // hash of function named, used to seed PRNG - nodeHash(node) { - const hashString = node.file + node.line; - let hash = 0; - for (let i = 0; i < hashString.length; i++) { - const char = hashString.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; - } - return hash; - } - // Simple PRNG from https://stackoverflow.com/a/47593316/12113178 - mulberry32(a) { - return function () { - let t = (a += 0x6d2b79f5); - t = Math.imul(t ^ (t >>> 15), t | 1); - t ^= t + Math.imul(t ^ (t >>> 7), t | 61); - return ((t ^ (t >>> 14)) >>> 0) / 4294967296; - }; - } - // modifies the normal color by three stable random values drawn from a - // PRNG seeded by the node hash - modifyNodeColorByHash(r, g, b, hash, range = 255) { - const rng = this.mulberry32(hash); - if (r === g && g === b) { - r = g = b = Math.min(255, Math.max(0, r + (rng() - 0.5) * range)); - } - else { - r = Math.min(range, Math.max(0, r + (rng() - 0.5) * range)); - g = Math.min(range, Math.max(0, g + (rng() - 0.5) * range)); - b = Math.min(range, Math.max(0, b + (rng() - 0.5) * range)); - } - return { - r, - g, - b, - }; - } - modifyNodeColorByCount(r, g, b, count, range = 255) { - r = Math.min(255, g * (1-count) ); - g = Math.min(255, g * (1-count) ); - b = Math.min(255, b * (1-count) ); - return { - r, - g, - b, - }; - } - nodeColors(node, hash) { - let r, g, b; - let a = 1; - /** - if (node.flags & 0x01) { - // runtime-dispatch - ; - ({ r, g, b } = this.modifyNodeColorByHash(204, 103, 103, hash, 20)); - } - else if (node.flags & 0x02) { - // gc - ; - ({ r, g, b } = this.modifyNodeColorByHash(204, 153, 68, hash, 20)); - } - else if (node.flags & 0x08) { - // compilation? - ; - ({ r, g, b } = this.modifyNodeColorByHash(100, 100, 100, hash, 60)); - } - else { - // default - ; - ({ r, g, b } = this.modifyNodeColorByHash(64, 99, 221, hash)); - } - if (node.flags & 0x10) { - // C frame - a = 0.5; - } - */ - - // if (node.count > 0) { - // better performance - ; - if (node.count_change / node.count > 100) { - ({ r, g, b } = this.modifyNodeColorByCount(0, 0, 0, node.count_change / node.count )); - } - else if (node.count_change/ node.count > 0) { - ({ r, g, b } = this.modifyNodeColorByCount(255, 255, 255, node.count_change / node.count )); - r = 255 - 20; - g = g - 20; - b = b - 20; - - } - else { - ({ r, g, b } = this.modifyNodeColorByCount(255, 255, 255, - node.count_change / node.count )); - b = 255 - 20; - g = g - 20; - r = r - 20; - } - - return { - fill: 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')', - stroke: 'rgba(' + 0.8 * r + ',' + 0.8 * g + ',' + 0.8 * b + ',' + a + ')', - text: 'rgba(255, 255, 255, ' + Math.max(0.6, a) + ')', - }; - } - drawGraph(node, width, height, x, y) { - if (!node) { - return; - } - this.canvasCtx.font = this.fontConfig; - this.canvasCtx.textBaseline = 'middle'; - if (y + this.boxHeight >= 0) { - const hash = this.nodeHash(node); - const { fill, stroke, text } = this.nodeColors(node, hash); - this.drawNode(node.func, fill, stroke, text, width, x, y); - } - node.pos = { - x, - y, - width, - height: this.boxHeight, - }; - if (y + this.boxHeight <= this.canvasHeight) { - for (const child of node.children) { - const w = width * (child.fraction || child.count / node.count); - this.drawGraph(child, w, height, x, y + this.boxHeight); - x += w; - } - } - else { - this.canWheelDown = true; - } - } - drawNode(text, color, bColor, textColor, width, x, y) { - if (width < 1) { - width = 1; - } - const drawBorder = false; //width > 20*this.borderWidth; - this.canvasCtx.fillStyle = color; - this.canvasCtx.beginPath(); - this.canvasCtx.rect(x, y + this.borderWidth, width, this.boxHeight - this.borderWidth); - this.canvasCtx.closePath(); - this.canvasCtx.fill(); - if (drawBorder) { - this.canvasCtx.fillStyle = bColor; - this.canvasCtx.beginPath(); - this.canvasCtx.rect(x, y + this.borderWidth, this.borderWidth, this.boxHeight - this.borderWidth); - this.canvasCtx.closePath(); - this.canvasCtx.fill(); - } - const textWidth = width - 2 * this.padding - 2 * this.borderWidth; - if (textWidth > 10) { - this.canvasCtx.save(); - this.canvasCtx.beginPath(); - this.canvasCtx.rect(x + this.borderWidth + this.padding, y + this.borderWidth + this.padding, textWidth, this.boxHeight - this.borderWidth - 2 * this.padding); - this.canvasCtx.closePath(); - this.canvasCtx.clip(); - this.canvasCtx.fillStyle = textColor; - this.canvasCtx.fillText(text, x + this.borderWidth + this.padding, y + this.boxHeight / 2 + this.borderWidth); - this.canvasCtx.restore(); - } - } - updateTooltip(node) { - this.tooltip.function.innerText = node.func; - if (node.file || node.line > 0) { - this.tooltip.file.innerText = node.file + ':' + node.line; - } - else { - this.tooltip.file.innerText = ''; - } - this.tooltip.count.innerText = (node.countLabel || (node.count + ' samples')).toString(); - let percentageText = ((100 * node.count) / - this.data[this.currentSelection].count).toFixed() + '% of root'; - if (this.activeNode.count != this.data[this.currentSelection].count) { - percentageText = percentageText + ', ' + ((100 * node.count) / - this.activeNode.count).toFixed() + '% of selection'; - } - this.tooltip.percentage.innerText = percentageText; - const flags = []; - if (node.flags & 0x01) { - flags.push('runtime-dispatch'); - } - if (node.flags & 0x02) { - flags.push('GC'); - } - if (node.flags & 0x08) { - flags.push('compilation'); - } - if (node.flags & 0x10) { - flags.push('task'); - } - let flagString = ''; - if (flags.length > 0) { - flagString = 'Flags: ' + flags.join(', '); - } - this.tooltip.flags.innerText = flagString; - if (this.ctrlClickHandler) { - this.tooltip['ctrlClickHint'].innerText = - 'Ctrl/Cmd+Click to open this file'; - } - } - drawHoverNode(node) { - this.hoverCanvasCtx.fillStyle = this.borderColor; - this.hoverCanvasCtx.fillRect(node.pos.x, node.pos.y + this.borderWidth, Math.max(1, node.pos.width), node.pos.height - this.borderWidth); - const innerWidth = node.pos.width - this.borderWidth * 2 * this.scale; - if (innerWidth > 1) { - this.hoverCanvasCtx.clearRect(node.pos.x + this.borderWidth * this.scale, node.pos.y + 2 * this.borderWidth * this.scale, innerWidth, node.pos.height - this.borderWidth * 3 * this.scale); - } - this.updateTooltip(node); - } - clearHover() { - this.hoverCanvasCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); - this.tooltipElement.style.display = 'none'; - } - drawHover(node, mouseX, mouseY) { - let found = false; - this.runOnNodeAtMousePosition(node, mouseX, mouseY, (node) => { - this.drawHoverNode(node); - found = true; - }); - return found; - } - runOnNodeAtMousePosition(root, x, y, f) { - if (x >= Math.floor(root.pos.x) && - x <= Math.ceil(root.pos.x + root.pos.width) && - y >= root.pos.y) { - if (y <= root.pos.y + root.pos.height) { - f(root); - return true; - } - else { - for (const child of root.children) { - if (this.runOnNodeAtMousePosition(child, x, y, f)) { - return true; - } - } - } - } - return false; - } - zoomInOnNode(node, mouseX, mouseY) { - let found = false; - this.runOnNodeAtMousePosition(node, mouseX, mouseY, (node) => { - this.clearHover(); - this.activeNode = node; - found = true; - }); - return found; - } - // ideally this wouldn't require tree traversal at all - findParentNode(target, current = null) { - if (current === null) { - current = this.data[this.currentSelection]; - } - for (const child of current.children) { - if (child === target) { - return current; - } - else { - const found = this.findParentNode(target, child); - if (found) { - return found; - } - } - } - return null; - } -} -//# sourceMappingURL=profile-viewer.js.map \ No newline at end of file diff --git a/perf/flame.jl b/perf/flame.jl deleted file mode 100644 index f849e82af4..0000000000 --- a/perf/flame.jl +++ /dev/null @@ -1,119 +0,0 @@ -# flame.jl: provides allocation breakdown for individual backtraces for single-process unthredded runs -# and check for overall allocation limits based on previous runs -# copied and modified from `ClimaAtmos/perf` -# -# To run this, add ProfileCanvas to the ClimaEarth environment - -import Test: @test, @testset -import ClimaAtmos as CA -import Profile -import ProfileCanvas -import YAML - -cc_dir = joinpath(dirname(@__DIR__)); -config_dir = joinpath(cc_dir, "config", "perf_configs"); -include(joinpath(cc_dir, "experiments", "ClimaEarth", "cli_options.jl")); - -# assuming a common driver for all tested runs -filename = joinpath(cc_dir, "experiments", "ClimaEarth", "run_amip.jl") - -# currently tested jobs and their allowed allocation limits -allocs_limit = Dict() -allocs_limit["perf_default_unthreaded"] = 8638304 -allocs_limit["perf_coarse_single_ft64"] = 18280800 -allocs_limit["perf_target_amip_n32_shortrun"] = 172134848 - -# number of time steps used for profiling -const n_samples = 2 - -# import parsed command line arguments -global parsed_args = parse_commandline(argparse_settings()) - -# select the configuration file and extract the job ID -config_file = - parsed_args["config_file"] = - isinteractive() ? "../config/perf_configs/perf_default_unthreaded.yml" : parsed_args["config_file"] -job_id = parsed_args["job_id"] - -# import config setup -config_dict = YAML.load_file(config_file) - -# global scope needed to recognize this definition in the coupler driver -parsed_args = merge(config_dict, parsed_args) - -# disable threading -parsed_args["enable_threading"] = false - -# flag to split coupler init from its solve -ENV["CI_PERF_SKIP_COUPLED_RUN"] = true - -@info job_id - -# initialize the coupler -try - include(filename) -catch err - if err.error !== :exit_profile_init - rethrow(err.error) - end -end - -##### -##### Profiling -##### - -function step_coupler!(cs, n_samples) - cs.tspan[1] = cs.model_sims.atmos_sim.integrator.t - cs.tspan[2] = cs.tspan[1] + n_samples * cs.Δt_cpl - solve_coupler!(cs) -end - -# compile coupling loop first -step_coupler!(cs, n_samples) -Profile.clear_malloc_data() -Profile.clear() - -# profile the coupling loop -prof = Profile.@profile begin - step_coupler!(cs, n_samples) -end - -# produce flamegraph -if haskey(ENV, "BUILDKITE_COMMIT") || haskey(ENV, "BUILDKITE_BRANCH") - output_dir = "perf/output/$job_id/" - mkpath(output_dir) - ProfileCanvas.html_file(joinpath(output_dir, "flame.html")) -else - ProfileCanvas.view(Profile.fetch()) -end - -##### -##### Allocation tests -##### - -# We're grouping allocation tests here for convenience. - -buffer = 1.4 # increase slightly for (nondeterministic) threaded runs - -# profile the coupling loop -allocs = @allocated step_coupler!(cs, n_samples) -@timev step_coupler!(cs, n_samples) - -@info "`allocs ($job_id)`: $(allocs)" - -if allocs < allocs_limit[job_id] * buffer - @info "TODO: lower `allocs_limit[$job_id]` to: $(allocs)" -end - -Δallocs = allocs / allocs_limit[job_id] -@info "Allocation change (allocs/allocs_limit): $Δallocs" -percent_alloc_change = (1 - Δallocs) * 100 -if percent_alloc_change ≥ 0 - @info "Allocations improved by: $percent_alloc_change %" -else - @info "Allocations worsened by: $percent_alloc_change %" -end - -@testset "Allocations limit" begin - @test allocs ≤ allocs_limit[job_id] * buffer -end diff --git a/perf/flame_diff.jl b/perf/flame_diff.jl deleted file mode 100644 index 84b3b52327..0000000000 --- a/perf/flame_diff.jl +++ /dev/null @@ -1,133 +0,0 @@ -# flame_diff.jl: provides allocation breakdown for individual backtraces for single-process unthredded runs -# and check for fractional change in allocation compared to the last staged run - -# The JSON package is needed to run this - -import ClimaAtmos as CA -import Profile -import Base: view -include("ProfileCanvasDiff.jl") -import .ProfileCanvasDiff -import JLD2 -import YAML - -if isinteractive() - buildkite_cc_dir = "." - scratch_cc_dir = "." - build_path = "0" -else - buildkite_branch = ENV["BUILDKITE_BRANCH"] - buildkite_commit = ENV["BUILDKITE_COMMIT"] - buildkite_number = ENV["BUILDKITE_BUILD_NUMBER"] - buildkite_build_path = ENV["BUILDKITE_BUILD_PATH"] - buildkite_pipeline_slug = ENV["BUILDKITE_PIPELINE_SLUG"] - buildkite_cc_dir = "/central/scratch/esm/slurm-buildkite/climacoupler-ci/" - scratch_cc_dir = joinpath(buildkite_build_path, buildkite_pipeline_slug) - build_path = - joinpath(buildkite_build_path, buildkite_pipeline_slug, buildkite_number, buildkite_pipeline_slug, "perf/") -end - -cwd = pwd() -@info "build_path is: $build_path" - -cc_dir = joinpath(dirname(@__DIR__)); -config_dir = joinpath(cc_dir, "config", "perf_configs"); -include(joinpath(cc_dir, "experiments", "ClimaEarth", "cli_options.jl")); - -# assuming a common driver for all tested runs -filename = joinpath(cc_dir, "experiments", "ClimaEarth", "run_amip.jl") - -# number of time steps used for profiling -n_samples = 2 - -# import parsed command line arguments -parsed_args = parse_commandline(argparse_settings()) - -# select the configuration file and extract the job ID -config_file = - parsed_args["config_file"] = - isinteractive() ? "../config/perf_configs/perf_default_unthreaded.yml" : parsed_args["config_file"] -job_id = parsed_args["job_id"] - -# import config setup -config_dict = YAML.load_file(config_file) - -# global scope needed to recognize this definition in the coupler driver -parsed_args = merge(config_dict, parsed_args) - -# disable threading -parsed_args["enable_threading"] = false - -# flag to split coupler init from its solve -ENV["CI_PERF_SKIP_COUPLED_RUN"] = true - -@info job_id - -function step_coupler!(cs, n_samples) - cs.tspan[1] = cs.model_sims.atmos_sim.integrator.t - cs.tspan[2] = cs.tspan[1] + n_samples * cs.Δt_cpl - solve_coupler!(cs) -end - -try # initialize the coupler - ENV["CI_PERF_SKIP_COUPLED_RUN"] = true - include(filename) -catch err - if err.error !== :exit_profile_init - rethrow(err.error) - end -end -##### -##### Profiling -##### - -# obtain the stacktree from the last saved file in `buildkite_cc_dir` -ref_file = joinpath(buildkite_cc_dir, "$job_id.jld2") - -if isfile(ref_file) - tracked_list = JLD2.load(ref_file) -else - tracked_list = Dict{String, Float64}() - @warn "FlameGraphDiff: No reference file: $ref_file found" -end - -# compile coupling loop first -step_coupler!(cs, n_samples) - -# clear compiler allocs -Profile.clear_malloc_data() -Profile.clear() - -# profile the coupling loop -prof = Profile.@profile begin - step_coupler!(cs, n_samples) -end - -# produce flamegraph with colors highlighting the allocation differences relative to the last saved run -# profile_data -if haskey(ENV, "BUILDKITE_COMMIT") || haskey(ENV, "BUILDKITE_BRANCH") - output_dir = "perf/output/$job_id" - mkpath(output_dir) - ProfileCanvasDiff.html_file( - joinpath(output_dir, "flame_diff.html"), - build_path = build_path, - tracked_list = tracked_list, - self_count = false, - ) - ProfileCanvasDiff.html_file( - joinpath(output_dir, "flame_diff_self_count.html"), - build_path = build_path, - tracked_list = tracked_list, - self_count = true, - ) -end - -# save (and reset) the stack tree if this is running on the `staging` branch -@info "This branch is: $buildkite_branch, commit $buildkite_commit" -profile_data, new_tracked_list = ProfileCanvasDiff.view(Profile.fetch(), tracked_list = tracked_list, self_count = true); -if buildkite_branch == "staging" - isfile(ref_file) ? - mv(ref_file, joinpath(scratch_cc_dir, "flame_reference_file.$job_id.$buildkite_commit.jld2"), force = true) : - nothing - JLD2.save(ref_file, new_tracked_list) # reset ref_file upon staging -end diff --git a/perf/flame_test.jl b/perf/flame_test.jl deleted file mode 100644 index 183269161f..0000000000 --- a/perf/flame_test.jl +++ /dev/null @@ -1,121 +0,0 @@ -# The JSON package is needed to run this - -import Test: @test, @testset -import Profile -import Base: view -include("ProfileCanvasDiff.jl") -import .ProfileCanvasDiff -import JLD2 - -if isinteractive() - buildkite_cc_dir = build_path = "." -else - buildkite_number = ENV["BUILDKITE_BUILD_NUMBER"] - buildkite_cc_dir = "/central/scratch/esm/slurm-buildkite/climacoupler-ci/" - build_path = "/central/scratch/esm/slurm-buildkite/climacoupler-ci/$buildkite_number/climacoupler-ci/perf/" -end - -output_dir = joinpath(buildkite_cc_dir, "test/") -mkpath(output_dir) - -# dummy functions to analyze -function cumsum_sqrt(x) - y = cumsum(x) - sqrt.(y) -end - -function get_y(x) - f = collect(1:1:10000) .* x - cumsum(f) -end - -function step_coupler!(n, y_all = []) - for i in collect(1:1:n) - y = get_y(i) - push!(y_all, y) - end - return y_all -end - -# init -step_coupler!(1) - -# clear compiler allocs -Profile.clear_malloc_data() -Profile.clear() - -# profile the coupling loop -prof = Profile.@profile begin - step_coupler!(100) -end - -# ref file -ref_file = joinpath(output_dir, "reference.jld2") -tracked_list = isfile(ref_file) ? JLD2.load(ref_file) : Dict{String, Float64}() - -# save ref file -profile_data, new_tracked_list = ProfileCanvasDiff.view(Profile.fetch(), tracked_list = tracked_list, self_count = true); - -JLD2.save(ref_file, new_tracked_list) # reset ref_file upon staging - - -""" - find_child(flame_tree, target_name; self_count = false) - -Helper function to find a particular flame tree node. -""" -function find_child(flame_tree, target_name; self_count = false) - - line = flame_tree.line - file = flame_tree.file - func = flame_tree.func - - node_name = "$func.$file.$line" - node_name = self_count ? "self_count_" * node_name : node_name - - if node_name == target_name - global out = flame_tree - else - if !(isempty(flame_tree.children)) - for sf in flame_tree.children - find_child(sf, target_name, self_count = self_count) - end - end - end - return @isdefined(out) && out -end - -@testset "flame diff tests" begin - # load the dictionary of tracked counts from the reference file - tracked_list = isfile(ref_file) ? JLD2.load(ref_file) : Dict{String, Float64}() - - test_func_name = "get_y.flame_test.jl.26" - - # test flame diff - tracked_list["$test_func_name"] = 100 # reference value for node count with child sum - profile_data, new_tracked_list = - ProfileCanvasDiff.view(Profile.fetch(), tracked_list = tracked_list, self_count = false) - node = find_child(profile_data.data["all"], test_func_name, self_count = false) - @test node.count_change == node.count - 100 - - # test flame diff with self_count - tracked_list["self_count_$test_func_name"] = 50 # reference value for node count w/o child sum - profile_data, new_tracked_list = - ProfileCanvasDiff.view(Profile.fetch(), tracked_list = tracked_list, self_count = true) - - node = find_child(profile_data.data["all"], test_func_name, self_count = false) - child_sum = sum(map(x -> x.count, node.children)) - @test node.count_change == (node.count - child_sum) - 50 - - # html_file test - ProfileCanvasDiff.html_file( - joinpath(output_dir, "flame_diff.html"), - build_path = build_path, - tracked_list = tracked_list, - self_count = false, - ) - - @test isfile(joinpath(output_dir, "flame_diff.html")) - rm(output_dir; recursive = true, force = true) - -end