This repository was archived by the owner on Oct 14, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
Added MS build benchmarking #23
Open
baimamboukar
wants to merge
118
commits into
main
Choose a base branch
from
msbuild-benchmarks
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
118 commits
Select commit
Hold shift + click to select a range
9339368
switch to new data source
baimamboukar 026d57b
init msbuild benchmarks
baimamboukar e8bb78c
setup msbuild workflow
baimamboukar a293e0e
added test solution dir
baimamboukar a5464f1
rm checking out to repo from workflow
baimamboukar 2e6399e
add argparser as requirement
baimamboukar 31a4778
refactored test_cases dir
baimamboukar e4dfca7
adding os as args
baimamboukar 11c43db
add arguments parser
baimamboukar 81de824
added repo cloner
baimamboukar e494a20
ellapse command format
baimamboukar e01e3bb
add build command
baimamboukar e2df984
add build command
baimamboukar 68da62b
added pylintrc
baimamboukar 380aede
added docstrings
baimamboukar 2cc946f
disable redefined-outer-name in linter'
baimamboukar cb914e4
updated branch
baimamboukar 6634cd3
fix linters
baimamboukar eabbebe
setup downloading and benchmarking logic
baimamboukar 97d9ed0
added unsuded arg ignore
baimamboukar 266940a
fix linters
baimamboukar 47d8adc
add urllib requirement
baimamboukar 4d4f1af
rm urllib
baimamboukar 9166ffe
rename job name
baimamboukar 0595fda
formatted versions
baimamboukar 919b0f2
reorganized matrix vars in arrays
baimamboukar a3834e9
resolve conflicts
baimamboukar d3ffa43
disbaled cron job
baimamboukar 1a11277
disable dispatch on PR
baimamboukar 214565c
disable workflow dispatch
baimamboukar 945e973
specified branches
baimamboukar 1582183
quoted urls
baimamboukar 3eeebbb
main update
baimamboukar 40ce470
separated jobs in os specific job
baimamboukar 7847286
simplify benchmarking windows
baimamboukar 30a3d9e
reformat windows benchmarking
baimamboukar 725ea83
refined bencharmk linux
baimamboukar 367aa07
remove unecessary parts
baimamboukar e21e674
fix: pylint errors
baimamboukar 588695e
format workflow
baimamboukar 8ee65aa
untrack DS_Store
baimamboukar 9d251c1
update extract commands
baimamboukar f190b2b
update repo path
baimamboukar bb11907
logging dir
baimamboukar a9eb2e7
switch and log dir
baimamboukar f9e5972
switched to simple command
baimamboukar 4d587b3
fix extract path
baimamboukar 116dba6
add dir
baimamboukar 05f31f4
create extract folders
baimamboukar 0d1d20d
update extracter
baimamboukar f6a8e9a
add verbose logs
baimamboukar 2a2a611
remove change dir command in download mehtod
baimamboukar 5935b43
add verbosity to clone
baimamboukar 9617b2b
reformat test solutions dirs
baimamboukar c917dbf
fix repo name
baimamboukar a0cfccd
revert cron job change
baimamboukar abbc894
apply review suggestions
baimamboukar 3ca2a14
switch to process run
baimamboukar 87bb527
switch to os.chdir
baimamboukar d002191
change to abspath
baimamboukar 41b69a5
revert chdir
baimamboukar 32a16c5
revert to parent dir
baimamboukar e94b3a8
add path specifier
baimamboukar 381f896
add full params to msbuild command
baimamboukar 28fa267
remove params
baimamboukar 39a6c35
added daily command run
baimamboukar c4d4736
rm return statement
baimamboukar 6df4e68
fix windows benchmarks
baimamboukar 240b4c4
add powershell flag to download command
baimamboukar 3ff0a1e
add powershell flag to extract command
baimamboukar 00d7d80
add docstring in windows script
baimamboukar 87af423
add build command to restore packages
baimamboukar 03538cd
rename scripts
baimamboukar 0e4a3ff
added script
baimamboukar 04688cd
added orleans case
baimamboukar c05546c
set to false
baimamboukar c5fb252
ci: skipping in orleans
baimamboukar 7d7fdc1
chore: change dotnet sdk
baimamboukar ebbdd03
add required version
baimamboukar 36a456b
restoring *build* command
baimamboukar 563ef54
add restore command
baimamboukar c25b884
rm daily version
baimamboukar c750986
git add .
baimamboukar 135911d
add orchardcore solution to testcases matrix
baimamboukar 626f6a6
remove bad chars
baimamboukar 0eee6ad
add daily version execution
baimamboukar 79daf10
rm sub dir in linux
baimamboukar c6eaab5
add saving feature to orchardmore solution
baimamboukar 963e8bd
format ordchardcore
baimamboukar 219d23a
added restore to linux
baimamboukar 4f0ab51
change msbuild csv path and rm build step
baimamboukar e9f7290
change msbuild path
baimamboukar d88be95
switch msbuild path to parent dir
baimamboukar efce7af
ci: add autocommit and issue creation job
baimamboukar 494c226
ci: disable windows large app with private packages solution
baimamboukar fbd3c2a
ci: added largeapp slim solution
baimamboukar 649fa46
ci: modularized benchmark runner
baimamboukar 451bdb3
ci: fix linters issues and updated sdk dir
baimamboukar bd5f760
ci: update exec path
baimamboukar 48ad415
ci: modularized ordchrdcore windows
baimamboukar 60319f9
ci: modularized all benchmarks
baimamboukar 6be33dd
fix nested cases dirs
baimamboukar 4ed61b6
ci: fix dotnet exec in windows nested runner
baimamboukar 4945056
ci: fix dotnet exec path in linux nested runner
baimamboukar 445a992
format: rm trailing spaces
baimamboukar 33e6ca3
update db file path
baimamboukar 0a05bcc
update zip extraction logic
baimamboukar 7f4f092
ci: add nuget solution
baimamboukar b989845
update csv file path
baimamboukar d96b478
change sdk version for nuget solution
baimamboukar b67fc65
change sdk to v8 in nuget solution
baimamboukar 001785d
clone with submodule
baimamboukar 8796bd3
change extract tar file logic
baimamboukar ca3c732
rm uncessary args
baimamboukar 780e60b
fix args formats
baimamboukar 090a910
rm nuget-win
baimamboukar 49b1332
add submodule initialization
baimamboukar 489eabb
fix: repo checkout
baimamboukar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| name: msbuild benchmarks | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| pull_request: | ||
|
|
||
| env: | ||
| DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true | ||
| DOTNET_CLI_TELEMETRY_OPTOUT: true | ||
| jobs: | ||
| msbuild_benchmarks_windows: | ||
| strategy: | ||
| matrix: | ||
| testcases: | ||
| - large_app_with_private_packages_centralised_ngbv_removed_windows | ||
| - orleans_windows | ||
| - orchardcore_windows | ||
| - nuget_windows | ||
| fail-fast: false | ||
| runs-on: windows-latest | ||
| steps: | ||
| #__________WE CHECK OUT TO WORKING REPO__________# | ||
| - name: Checkout | ||
| uses: actions/checkout@v3 | ||
|
|
||
| #__________WE GET THINGS READY TO RUN PYTHON SCRIPTS__________# | ||
| - name: setup python | ||
| uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: 3.11 | ||
|
|
||
| #__________FINALLY WE RUN BENCHMARK TEST AND MEASURE EXECUTION TIME__________# | ||
| - name: run benchmark for windows | ||
| working-directory: scripts/test_cases/ | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install -r requirements.txt | ||
| python "${{matrix.testcases}}.py" | ||
|
|
||
| msbuild_benchmarks_linux: | ||
| strategy: | ||
| matrix: | ||
| testcases: | ||
| - large_app_with_private_packages_centralised_ngbv_removed_linux | ||
| - orleans_linux | ||
| - orchardcore_linux | ||
| #- nuget_linux | ||
| fail-fast: false | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| #__________WE CHECK OUT TO WORKING REPO__________# | ||
| - name: Checkout | ||
| uses: actions/checkout@v3 | ||
|
|
||
| #__________WE GET THINGS READY TO RUN PYTHON SCRIPTS__________# | ||
| - name: setup python | ||
| uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: 3.11 | ||
|
|
||
| #__________FINALLY WE RUN BENCHMARK TEST AND MEASURE EXECUTION TIME__________# | ||
| - name: run benchmark for linux | ||
| working-directory: scripts/test_cases/ | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| pip install -r requirements.txt | ||
| python "${{matrix.testcases}}.py" | ||
|
|
||
| process_data: | ||
| # Only run on main repository | ||
| # Scheduled workflows do not have information about fork status, hence the hardcoded check | ||
| if: github.repository == 'G-Research/DotNetPerfMonitor' && github.ref == 'refs/heads/main' | ||
| environment: updater | ||
| runs-on: ubuntu-latest | ||
| needs: | ||
| - msbuild_benchmarks_windows | ||
| - msbuild_benchmarks_linux | ||
| steps: | ||
| - name: Install Step CLI | ||
| env: | ||
| VERSION: 0.19.0 | ||
| run: | | ||
| curl -sLO https://github.com/smallstep/cli/releases/download/v${VERSION}/step-cli_${VERSION}_amd64.deb | ||
| sudo dpkg -i step-cli_${VERSION}_amd64.deb | ||
| rm step-cli_${VERSION}_amd64.deb | ||
|
|
||
| - name: Create access token | ||
| id: token | ||
| env: | ||
| APP_ID: ${{ secrets.APP_ID }} | ||
| APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} | ||
| run: | | ||
| jwt=$(step crypto jwt sign --key /dev/fd/3 --issuer $APP_ID --expiration $(date -d +5min +%s) --subtle 3<<< $APP_PRIVATE_KEY) | ||
| installation_id=$(curl -s -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer $jwt" https://api.github.com/app/installations | jq '.[] | select(.account.login == "${{ github.repository_owner }}") | .id') | ||
| token=$(curl -s -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer $jwt" https://api.github.com/app/installations/$installation_id/access_tokens | jq -r '.token') | ||
| echo "::add-mask::$token" | ||
| echo "token=$token" >> $GITHUB_OUTPUT | ||
|
|
||
| - name: Checkout | ||
| uses: actions/checkout@v3 | ||
| with: | ||
| token: ${{ steps.token.outputs.token }} | ||
|
|
||
| - uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: 3.11 | ||
|
|
||
| - uses: actions/download-artifact@v3 | ||
| with: | ||
| path: artifacts | ||
|
|
||
| - name: Install python3 dependencies | ||
| working-directory: scripts/processors/ | ||
| run: pip3 install -r requirements.txt | ||
|
|
||
| - name: Process data | ||
| shell: pwsh | ||
| run: | | ||
| Get-ChildItem -File -Path artifacts -Depth 1 | Foreach {. python3 scripts/processors/process_results.py $_.fullname data/msbuild.csv} | ||
|
|
||
| - name: Get alerts | ||
| id: alerts | ||
| run: | | ||
| python3 scripts/processors/generate_alert.py data/nuget.csv active_msbuild_regressions.txt | ||
| if test -f "active_msbuild_regressions.txt"; then | ||
| echo "file_exists=true" >> $GITHUB_OUTPUT | ||
| echo "file_content='$file_content'" >> $GITHUB_OUTPUT | ||
| fi | ||
|
|
||
| - name: Create new issue if necessary | ||
| if: steps.alerts.outputs.file_exists == 'true' | ||
| run: | | ||
| numOpenIssues="$(gh api graphql -F owner=$OWNER -F name=$REPO -f query=' | ||
| query($name: String!, $owner: String!) { | ||
| repository(owner: $owner, name: $name) { | ||
| issues(states:OPEN, filterBy: { labels: ["active_msbuild_regressions"]}){ | ||
| totalCount | ||
| } | ||
| } | ||
| } | ||
| ' --jq '.data.repository.issues.totalCount')" | ||
|
|
||
| if [ $numOpenIssues -eq 0 ]; then | ||
| echo "Creating new issue" | ||
| gh issue create --title "Active MS Build regression" --label "active_msbuild_regressions" --body "\`\`\` $(cat active_msbuild_regressions.txt) \`\`\`" --repo $GITHUB_REPOSITORY | ||
| fi | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| OWNER: ${{ github.repository_owner }} | ||
| REPO: ${{ github.event.repository.name }} | ||
|
|
||
| - uses: stefanzweifel/git-auto-commit-action@v4 | ||
| with: | ||
| file_pattern: "data/msbuild.csv" | ||
| commit_author: "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" | ||
|
|
||
| - name: Upload data | ||
| if: steps.alerts.outputs.file_exists == 'true' | ||
| uses: actions/upload-artifact@v3 | ||
| with: | ||
| name: active_msbuild_regressions | ||
| path: active_msbuild_regressions.txt |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| .idea | ||
| .DS_Store | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| [pylint.messages control] | ||
| disable = unused-variable, line-too-long, invalid-name | ||
| disable = unused-variable, line-too-long, invalid-name, redefined-outer-name, unused-argument, duplicate-code, too-many-locals, too-many-arguments |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| """Run a benchmark and return the results.""" | ||
| import argparse | ||
| import tarfile | ||
| import subprocess | ||
| import time | ||
| import os | ||
| import urllib.request | ||
| import benchmark_utils as utils | ||
|
|
||
|
|
||
| def create_extract_destinations(path): | ||
| """ Create the extract destination directories if they do not exist""" | ||
| if not os.path.exists(path): | ||
| os.mkdir(path) | ||
| os.chdir(path) | ||
| if not os.path.exists("base"): | ||
| os.mkdir("base") | ||
| if not os.path.exists("daily"): | ||
| os.mkdir("daily") | ||
|
|
||
|
|
||
| def download_file(url, filename): | ||
| """ Download file from url and save it to filename""" | ||
| with urllib.request.urlopen(url) as response, open(filename, 'wb') as out_file: | ||
| data = response.read() | ||
| out_file.write(data) | ||
|
|
||
|
|
||
| def extract_tar_gz(file_path, extract_path): | ||
| """Extract a zip file to the specified destination path.""" | ||
| with tarfile.open(file_path, 'r:gz') as tar: | ||
| tar.extractall(path=extract_path) | ||
|
|
||
|
|
||
| def download_and_extract_dotnet_sdk(version_url, extract_path): | ||
| """ Download and extract the dotnet sdk""" | ||
|
|
||
| tar_gz_file = "dotnet-sdk.tar.gz" | ||
| download_file(version_url, tar_gz_file) | ||
|
|
||
| # Extract the tar.gz file | ||
| extract_tar_gz(tar_gz_file, extract_path) | ||
| # extract_command = f"tar -xzf {tar_gz_file} -C {extract_path}" | ||
| # subprocess.run(["tar", "-xzf", tar_gz_file, | ||
| # "-C", extract_path], check=True) | ||
|
|
||
|
|
||
| def run_build_to_restore_packages(dotnet_executable): | ||
| """_summary_ | ||
|
|
||
| Args: | ||
| dotnet_executable (_type_): _description_ | ||
| """ | ||
| print('-----_restoting packages_-----') | ||
| subprocess.run([dotnet_executable, 'restore'], check=True) | ||
| subprocess.run([dotnet_executable, 'build'], check=True) | ||
|
|
||
|
|
||
| def measure_execution_time(command): | ||
| """measure_execution_time runs build command and measure its execution time""" | ||
|
|
||
| # Record start time | ||
| start_time = time.time() | ||
|
|
||
| # Run the command | ||
| subprocess.call(command, shell=True) | ||
|
|
||
| # Calculate elapsed time | ||
| end_time = time.time() | ||
| elapsed_time = end_time - start_time | ||
|
|
||
| return elapsed_time | ||
|
|
||
|
|
||
| def clone_repository(repo_url, test_repo_name, test_repo_path, nested, test_solution_dir, commit_hash): | ||
| """_summary_ | ||
|
|
||
| Args: | ||
| repo_url (String): url of the repository to be cloned | ||
| repo_path (String): path containing test code | ||
| """ | ||
| # Clone the repository containing the solution | ||
| os.chdir('..') | ||
| subprocess.run(['git', 'clone', '--recursive', repo_url], check=True) | ||
|
|
||
| # --- checkout to the commit hash if it is not empty ---- # | ||
| if commit_hash != '': | ||
| subprocess.run(['git', 'submodule', 'update', | ||
| '--init', '--recursive'], check=True) | ||
| # os.chdir(test_repo_name) | ||
|
|
||
| subprocess.run(['git', 'checkout', commit_hash], check=True) | ||
|
|
||
| os.chdir(test_repo_name) | ||
| if nested: | ||
| os.chdir(test_repo_path) | ||
| os.chdir(test_solution_dir) | ||
| subprocess.run(['ls'], check=True) | ||
|
|
||
|
|
||
| def run_benchamrk(args): | ||
| """_summary_ | ||
| run_benchamrk() | ||
|
|
||
| """ | ||
| # Access the arguments as attributes of the 'args' object | ||
| EXTRACT_PATH = args.extract_path | ||
| DOTNET_BASE_VERSION_URL_LINUX = args.dotnet_base_version_url_linux | ||
| DOTNET_DAILY_VERSION_URL_LINUX = args.dotnet_daily_version_url_linux | ||
| TEST_SOLUTION_REPO_URL = args.test_solution_repo_url | ||
| TEST_SOLUTION_CASE = args.test_solution_case | ||
| TEST_SOLUTION_DIR = args.test_solution_dir | ||
| TEST_SOLUTION_FILE = args.solution_file | ||
| COMMIT_HASH = args.commit_hash | ||
| DATABASE_FILE = args.database_file | ||
| NESTED = args.is_nested_solution == "True" | ||
| TEST_REPO_NAME = utils.get_repo_name_by_url(TEST_SOLUTION_REPO_URL) | ||
|
|
||
| # create the extract destination directories if they do not exist | ||
| create_extract_destinations(EXTRACT_PATH) | ||
|
|
||
| # download and extract the dotnet sdk | ||
|
|
||
| download_and_extract_dotnet_sdk(DOTNET_BASE_VERSION_URL_LINUX, "base") | ||
| download_and_extract_dotnet_sdk(DOTNET_DAILY_VERSION_URL_LINUX, "daily") | ||
|
|
||
| # clone the repository and navigate to the solution directory | ||
| clone_repository(TEST_SOLUTION_REPO_URL, | ||
| TEST_REPO_NAME, TEST_SOLUTION_CASE, NESTED, TEST_SOLUTION_DIR, COMMIT_HASH) | ||
|
|
||
| # build the solution using the base version | ||
|
|
||
| msbuild_command = f"msbuild {TEST_SOLUTION_FILE}" | ||
| versions = ['base'] | ||
| duration_in_seconds = 0 | ||
| base_duration_in_seconds = 0 | ||
| test_scenario = 'cold' | ||
| for version in versions: | ||
| # sub_dir = "/sdk" if version == 'daily' else '' | ||
| # subdirs = './../../../sdk' if NESTED else './../sdk' | ||
| subdirs = './../../../sdk' if NESTED else './../sdk' | ||
| exec_path = os.path.abspath(f"{subdirs}/{version}/dotnet") | ||
| run_build_to_restore_packages(exec_path) | ||
| simple_command = f"msbuild {TEST_SOLUTION_FILE}" | ||
| command = f"{exec_path} {simple_command}" | ||
| elapsed_time = measure_execution_time(command) | ||
| if version == 'base': | ||
| base_duration_in_seconds = elapsed_time | ||
| else: | ||
| duration_in_seconds = elapsed_time | ||
| print('-----LINUX BENCHMARK RESULT-----') | ||
| print( | ||
| f"Running '{command}' with {version} version took {elapsed_time}s to execute.") | ||
|
|
||
| # save benchmark results to a csv file | ||
| SDK_VERSION = subprocess.check_output( | ||
| [exec_path, '--version'], text=True, stderr=subprocess.STDOUT).strip() | ||
| SDK_DAILY_VERSION = "8.0.1xx" | ||
| utils.save_benchmark_results( | ||
| DATABASE_FILE, duration_in_seconds, base_duration_in_seconds, SDK_VERSION, SDK_DAILY_VERSION, TEST_SOLUTION_CASE, test_scenario | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| # Parse arguments using argparse | ||
| parser = argparse.ArgumentParser( | ||
| description='---Benchmark Runner---') | ||
|
|
||
| # <-Parse arguments | ||
| parser.add_argument('--extract_path', help='Path for extraction') | ||
| parser.add_argument('--dotnet_base_version_url_linux', | ||
| help='URL for the base version of .NET SDK on Linux') | ||
| parser.add_argument('--dotnet_daily_version_url_linux', | ||
| help='URL for the daily version of .NET SDK on Linux') | ||
| parser.add_argument('--test_solution_repo_url', | ||
| help='URL of the test solution repository') | ||
| parser.add_argument('--test_solution_case', help='Test solution case') | ||
| parser.add_argument('--test_solution_dir', help='Test solution directory') | ||
| parser.add_argument('--database_file', help='Path to the database file') | ||
| parser.add_argument('--solution_file', help='Path to the database file') | ||
| parser.add_argument('--is_nested_solution', | ||
| help='Path to the database file') | ||
| parser.add_argument('--commit_hash', default='', | ||
| help='Path to the database file') | ||
| # Parse arguments -> | ||
|
|
||
| args = parser.parse_args() | ||
|
|
||
| run_benchamrk(args) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍