Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved cross platform metric collection #2834

Merged
merged 10 commits into from
Mar 3, 2025

Conversation

NathanSavageKaimai
Copy link
Contributor

This PR improves CPU and RAM metric collection across multiple environments. The CPU metrics are now fully cGroup aware report properly in containerised environments with cpu quota limits. The memory profile method on windows has been updated from using legacy WMIC to powershell. Finally two new utility methods have been added to @crawlee/utils, general.ts to determine if the scraper is containerised (instead of just running in docker) as well as if cGroup is enabled.

Adds

@crawlee/utils

general.ts

  • isContainerised() an extention of isDocker() that also checks for the presence of a KUBERNETES_SERVICE_HOST environment variable for k8 and a CRAWLEE_CONTAINERISED environment variable for manual control.
  • getCgroupsVersion() a method to determine the cGroup version in a cGroup controlled environment. It does this by checking for a file at /sys/fs/cgroup/memory/. If it is present, the cGroup verison is 1, else version 2.

cpu-info.ts

Collects cpu infomation in a similar manner to memory-info.ts

  • getCurrentCpuTicks() The existing solution. Used in AWS lambda, containerised environments without a cGroup cpu limit and on bare metal.
  • getCpuQuota() Gets the cpu quota in cGroup controlled environments.
  • getCpuPeriod() Gets the cpu quota period in cGroup controlled environments.
  • getContainerCpuUsage() Gets the containers cpu usage.
  • getSystemCpuUsage() Gets the systems cpu usage.
  • getCpuInfo() The main method for collecting cpu load metrics. Determines the enviroment and calls the other functions accordingly.

Removes

@apify/ps-tree

A legacy package that checked memory usage using WMIC.exe (depreciated) on windows or ps on *nix. Replaced by @crawlee\packages\utils\src\internals\psTree.ts which calculates the memory usage in a similar manner but using powershell and Get-CimInstance Win32_Process. Also adds type safety.

Fixes

Fixes: #2771

@NathanSavageKaimai
Copy link
Contributor Author

hey all, thanks for agreeing to take a look. I havent done much OSS before so im looking forward to hearing your thoughts :). Assuming it all looks good to you, when might it be incorperated into a crawlee release? At work we have a project that hinged on using crawlee in k8 so the autoscaling issues in containers is causing a fairly significant issue for us.

When you come to review it, id be happy to hop on a discord call and discuss it. :)

Thanks for everything you do!

@janbuchar
Copy link
Contributor

Hi @NathanSavageKaimai and thanks for your willingness to contribute! In the issue that this aims to close, you mentioned the possibility of using the ps-list package. If we decided that adding another dependency is fine, could the change be smaller? How much? Are there any other tradeoffs or possible disadvantages to using that library?

@NathanSavageKaimai
Copy link
Contributor Author

hi @janbuchar, ps-list would serve the same purpose as the new packages/utils/src/internals/psTree.ts file so it would be a 170 line reduction. Another module that might be useful that i have found since is systeminfomation. This module could likley replace, cpu-info.ts, memory-info.ts and psTree.ts leading to a ~540 line reduction. :)

Let me know if you would like me to explore these options.

@NathanSavageKaimai
Copy link
Contributor Author

with ps-list, you are relying on a third party binary which doesnt provide its source code as far as i can tell. It could be a potential supply chain risk.

@vladfrangu
Copy link
Member

I will +1 that worry, I'm not a fan of using a dependency that embeds a binary whose source code isn't directly open source / one we could build ourselves and embed

@janbuchar
Copy link
Contributor

Um, as far as I can tell, ps-list uses fastlist, which seems open enough to me - am I missing anything?

@NathanSavageKaimai
Copy link
Contributor Author

@janbuchar Ah yep, i hadnt found the cpp repo. still, being externally tracked, theres no automatic method to verify the authenticity of the binary beyond downloading from both sources and checking the hashes.

@vladfrangu
Copy link
Member

Um, as far as I can tell, ps-list uses fastlist, which seems open enough to me - am I missing anything?

The fact the binary is just embedded in instead of precompiled (like impit) or built at install time is a worry imo

@janbuchar
Copy link
Contributor

You both make a good point. I'd still consider exploring systeminformation - if we can avoid maintaining code for reading low level system details, it might be worth the increased install size.

@NathanSavageKaimai
Copy link
Contributor Author

You both make a good point. I'd still consider exploring systeminformation - if we can avoid maintaining code for reading low level system details, it might be worth the increased install size.

cool will do. :)

@NathanSavageKaimai
Copy link
Contributor Author

@janbuchar ive had a play around with systeminfomation and unfortunately it isnt as useful as i hoped it would be. It seems that the "docker" functions arent generating the metrics themselves but sending requests to the docker socket for the data. This unfortunately means that short of mounting the socket within the container, it can only work on the host. I have done a good search and as far as i can tell, there is no universal, cross platform library to collect cpu and ram metrics on "bare metal", cgroup 1 and 2. There might even be scope here for an entirely new package for apify but for now, my approach seems to be the best one going forward. :)

@janbuchar
Copy link
Contributor

Sounds reasonable, thanks! I will try to review the code in depth this week.

@NathanSavageKaimai
Copy link
Contributor Author

hey @janbuchar have you been able to have a look yet? No worries ethier way. if you like, we can sit down for a call later. Just shoot me a message on Discord - crafty5064. Im free until 2pm UTC or all day Saturday. :)

@NathanSavageKaimai
Copy link
Contributor Author

Hi all,

I hope you had a good weekend! Have you had a chance to review these changes? I was hoping they might be included in a release soon as these issues are blocking my company from deploying our product fully to k8. If you would like a chat, im free untill 1pm utc tomorrow. :)

@janbuchar
Copy link
Contributor

Hi, I'll look into it tomorrow. Sorry for the delay!

Copy link
Contributor

@janbuchar janbuchar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job on this one! I have a bunch of readability/code structure comments. More importantly though, the tests here are very narrowly scoped and use mocking heavily. Do you think you could add an E2E test that would verify that the system information detection works as expected? Feel free to suggest any other way to test this as a whole.

@@ -43,6 +43,35 @@ export async function isDocker(forceReset?: boolean): Promise<boolean> {
return isDockerPromiseCache;
}

/**
* Returns a `Promise` that resolves to true if the code is running in a containerised environment.
* Returns true if the CRAWLEE_CONTAINERISED environment variable is set.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is supposed to set the CRAWLEE_CONTAINERISED environment variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is meant to be a manual way to run the containerised resource checks in case the other heuristics dont catch it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. Could you add it to the documentation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you mind showing me where? im a little bit lost on that side of it, ta.

Im free for a call right now if you would like a chat. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* @param includeRoot - Optional flag. When true, include the process with the given PID if found.
* Defaults to false.
*/
export async function psTree(pid: number | string, includeRoot: boolean = false): Promise<ProcessInfo[]> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very long and complex function. Possibly the main reason why it's hard to read is that it combines the implementation for UNIX and Windows in a single function. Could it be broken down into multiple smaller functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was pretty much a copy paste from apify/pstree with the WMIC changes. I can reformat it. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do that, apify/pstree is super dated and I'm sure that if we don't refactor it now, we won't get back to it, ever.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool will do :)

@NathanSavageKaimai
Copy link
Contributor Author

hi @janbuchar thanks for your insight! Most of the points were just me trying to follow conventions set out in the prexisting memory-info.ts file but i will definitely work on clarifying it. :)

@janbuchar
Copy link
Contributor

@NathanSavageKaimai please look into refactoring of psTree so that it's more readable.

Also, any thoughts regarding this?

More importantly though, the tests here are very narrowly scoped and use mocking heavily. Do you think you could add an E2E test that would verify that the system information detection works as expected? Feel free to suggest any other way to test this as a whole.

@NathanSavageKaimai
Copy link
Contributor Author

@NathanSavageKaimai please look into refactoring of psTree so that it's more readable.

Also, any thoughts regarding this?

More importantly though, the tests here are very narrowly scoped and use mocking heavily. Do you think you could add an E2E test that would verify that the system information detection works as expected? Feel free to suggest any other way to test this as a whole.

its a difficult one given that its so close to the metal, an e2e test would be dependant on the current state of the test runner unless i mocked the exec call and readline interface but at that point it may as well be a unit test. Also, personally I am only set up to run tests on windows or linux through wsl so i cant verify Macos compatability beyond "its a copy paste from a solution that persumably worked". What i will do is reimplement the unit tests in apify/pstree

@janbuchar
Copy link
Contributor

What i will do is reimplement the unit tests in apify/pstree

Cool, that will at least give us some certainty that ps-tree works.

I guess we could make a script that would show the current CPU and memory usage ratio and compare it with the old implementation. Then we could at least test-drive this on several machines with different OS and see if it behaves reasonably. What do you think?

@janbuchar
Copy link
Contributor

Hi @NathanSavageKaimai, we discussed this PR with @B4nan. Since this change impacts critical parts of Crawlee's functionality and testing it deeply enough requires time that we don't currently have (taking into account that you need this released soon), would you be open to making the new system info implementation opt-in using some kind of a feature flag?

If yes, we could probably release it and test it later, before we decide to make the new implementation the default.

@NathanSavageKaimai
Copy link
Contributor Author

Hi @janbuchar, sounds good, do you want me to make it an experiment? Also, any preference on what i call the flag?

Ta

@janbuchar
Copy link
Contributor

Yup, CrawlerExperiments sounds like the way to go. If you can't think of anything more descriptive, systemInfoNG or systemInfoV2 work just fine for me 🙂

@NathanSavageKaimai
Copy link
Contributor Author

The original metric collection has been restored and the new solution made toggleable with the systemInfoV2 experimental feature flag. I ended up needing to add the systemInfoV2 flag to the configuration class as localEventsManager isnt tied to a particular scraper. The only potential issue with this is that if someone was running multiple scrapers, some with the experiment and others without, it would use whichever setting was on the crawler instanciated last.

@NathanSavageKaimai
Copy link
Contributor Author

hi, just been doing some more tests and i found an issue, please dont merge yet.

ta

@NathanSavageKaimai
Copy link
Contributor Author

hi again, @janbuchar. I have fixed the scaling error. If you are all happy about it, perhaps we could merge it in soon? Thanks. :)

@janbuchar
Copy link
Contributor

Perfect! I think there are no major issues. We agreed with @B4nan that he'll quickly scan it and, if everything is fine, merge it. In the meantime, I'll try and test some risky changes we have waiting in the release branch so that we can make a stable release.

Thanks again for your contribution and for bearing with us 🙂

@NathanSavageKaimai
Copy link
Contributor Author

sounds good, just one more thing i have noticed, currently, the "TICKS_PER_SECOND" is hardcoded to 100, im going to introduce a quick check to read it from the kernel as i have found out that in some systems, rarely it can be different.

@NathanSavageKaimai
Copy link
Contributor Author

hi @B4nan, thanks for running the pull request toolkit, have you been able to have a look over the changes? No worries ethier way. :)

@NathanSavageKaimai
Copy link
Contributor Author

Hi @janbuchar have you been able to talk with @B4nan? my boss is asking for a timeline.

Ta

@B4nan
Copy link
Member

B4nan commented Feb 26, 2025

You will need to wait a bit, the PR is large, so it takes time. If you need to adopt this in your own project asap, I'd suggest you use something like https://github.com/ds300/patch-package instead of pushing us for merging it early.

@NathanSavageKaimai
Copy link
Contributor Author

Apologies, i didnt mean to offend. My understanding of the situation was that you were happy with janbuchar's review and you were going to simply scan it.

Thanks for all you are doing.

Copy link
Member

@B4nan B4nan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left a few comments, haven't seen the whole thing yet, but it's looking pretty good. i'd remove the experiments option in favor of the configuration one, having both of them feels weird, this is surely a system-wide option, so the config class is more appropriate for it

Comment on lines 97 to 98
log.exception(err as Error, 'Cpu snapshot failed.');
return {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a good idea? downstream will expect values, and end up working with undefined instead

unless there is a good reason, i would remove the try/catch here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do, i put that for parity with the existing createMemoryInfo function

Comment on lines 60 to 63
if (process.env.CRAWLEE_CONTAINERIZED) {
isContainerizedResult = !FALSY_REGEX.test(process.env.CRAWLEE_CONTAINERIZED);
return isContainerizedResult;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this would be handled via configuration class too, which normalizes env vars

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately, we cannot access the Configuration class in utils, it would create a circular dependancy. I could refactor it into core?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, right, i guess it can stay this way too, not a huge deal. we can polish this in v4 if we decide to enable this by default (and likely remove the old implementation)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the commit i pushed earlier, i made it so that the config flag had no effect on isContainerized but was checked in LocalEventManager everywhere that isContainerized would be used. Which approach do you prefer?

@NathanSavageKaimai
Copy link
Contributor Author

hi @B4nan, working on those changes now. If we only want the configuration feature flag and not the experiment one, do you still want the experiment documentation page or shoudl i move it somewhere else?

Ta

@B4nan
Copy link
Member

B4nan commented Feb 27, 2025

yeah, let's keep the docs, that's fine

 - removed systemInfoV2 experiment flag in favour of config flag
 - added containerized / CRAWLEE_CONTAINERIZED config flag
 - isContainerized no longer checks the 'containerized' flag, instead 'containerized' is used in place of isContainerized in LocalEventManager
Copy link
Member

@B4nan B4nan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all right, I guess it all looks fine, especially given its opt-in. lets give this a test run.

thanks a lot for the PR, overall great job!

@B4nan B4nan requested review from janbuchar and removed request for janbuchar and vladfrangu March 3, 2025 15:21
@B4nan B4nan dismissed janbuchar’s stale review March 3, 2025 15:22

so i can merge :]

@B4nan B4nan merged commit e41b2f7 into apify:master Mar 3, 2025
9 checks passed
@NathanSavageKaimai
Copy link
Contributor Author

all right, I guess it all looks fine, especially given its opt-in. lets give this a test run.

thanks a lot for the PR, overall great job!

Great! Thanks for your review! Just so i understand this correctly, the production build should become immediately available as beta 28 on npm?

Thanks!

@B4nan
Copy link
Member

B4nan commented Mar 3, 2025

Yes, and we'll ship a new stable likely this afternoon or tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Get rid of dependency on deprecated WMIC on Windows
4 participants