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

[Brave News]: Build feed off the main thread #21833

Merged
merged 18 commits into from
Feb 7, 2024
Merged

Conversation

fallaciousreasoning
Copy link
Contributor

@fallaciousreasoning fallaciousreasoning commented Jan 31, 2024

Resolves brave/brave-browser#35695

Submitter Checklist:

  • I confirm that no security/privacy review is needed and no other type of reviews are needed, or that I have requested them
  • There is a ticket for my issue
  • Used Github auto-closing keywords in the PR description above
  • Wrote a good PR/commit description
  • Squashed any review feedback or "fixup" commits before merge, so that history is a record of what happened in the repo, not your PR
  • Added appropriate labels (QA/Yes or QA/No; release-notes/include or release-notes/exclude; OS/...) to the associated issue
  • Checked the PR locally:
    • npm run test -- brave_browser_tests, npm run test -- brave_unit_tests wiki
    • npm run lint, npm run presubmit wiki, npm run gn_check, npm run tslint
  • Ran git rebase master (if needed)

Reviewer Checklist:

  • A security review is not needed, or a link to one is included in the PR description
  • New files have MPL-2.0 license header
  • Adequate test coverage exists to prevent regressions
  • Major classes, functions and non-trivial code blocks are well-commented
  • Changes in component dependencies are properly reflected in gn
  • Code follows the style guide
  • Test plan is specified in PR before merging

After-merge Checklist:

Test Plan:

  1. In a new profile click the Learn More link on the Enable Brave News popup
  2. Navigate back
  3. The page should not crash
  4. Enable Brave News
  5. The publishers should load
  6. The feed should load
  7. Disable Brave news
  8. Enable Brave news
  9. THe publishers should load
  10. THe Feed should load
  11. Disable Brave news, refresh the page
  12. Enable Brave news
  13. The publishers should load
  14. The feed should load

@fallaciousreasoning fallaciousreasoning changed the title [Brave News]: No prefetch on enable [Brave News]: No prefetch on first load Jan 31, 2024
@petemill
Copy link
Member

Looks like we definitely don't need this part, but also seems like it doesn't remove the ~startup requests for data since the TabHelper (created for every Tab's WebContents) ensures the data has started to be fetched in its ctor. And this data is needed for the feature to work. So, unless we're going to delay using the feature away from the 1st WebContents, then we should probably also make sure we're doing all we can to not block the main thread when requesting, parsing or processing the publisher or feed data.

@fallaciousreasoning fallaciousreasoning changed the title [Brave News]: No prefetch on first load [Brave News]: Build feed off the main thread Jan 31, 2024
Copy link
Member

@petemill petemill left a comment

Choose a reason for hiding this comment

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

Seems sensible to remove the prefetch and do that separately if the thread moving is easier to reason

@petemill petemill requested a review from atuchin-m January 31, 2024 23:20
Comment on lines 1257 to 1259
auto articles = GetArticleInfos(
builder.publishers_controller_->GetLastLocale(), feed_items,
builder.publishers_controller_->GetLastPublishers(), builder.signals_);
Copy link
Contributor

Choose a reason for hiding this comment

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

Regardless of logical invariant of the builder's state,
to me it feel like accessing builder's member variable and publisher_controller is exposing us to potential risk of thread safety. If we're to do this, PublisherController's GetLastLocale() and others should be guaranteed of thread safety.

I think bind all recipes like publisher ids, locale, last publishers, signal from the main thread would be much safer.

(GenerateAllfeed as well)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay cool, I was considering that but got scared off by the fact that we were doing this for perf. Honestly, it'd make me feel a lot better about this to do it that way 👍

Copy link
Contributor

@sangwoo108 sangwoo108 Feb 1, 2024

Choose a reason for hiding this comment

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

got scared off by the fact that we were doing this for perf

Yeah, now I see where you're coming from. Your concern makes sense when data is huge. I think we can try using RefCountedThreadSafe and others. Let me try a bit.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I suggest not use RefCountedThreadSafe, it makes the code much harder to maintain in terms of avoiding data races.

There are a few alternatives:

  1. Post everything each time you need as extra arguments as a copy in function/static method/lambda (depends on the data, could be costly).
  2. Make a task runner inner class with necessary data. See FooService here: https://source.chromium.org/chromium/chromium/src/+/main:docs/threading_and_tasks_testing.md?q=OnTaskRunnerDeleter%20lang:md

@fallaciousreasoning
Copy link
Contributor Author

@sangwoo108 thanks for the feedback - I'm going to copy everything into the lambda. I've got some compile errors, so going to push tomorrow 😄

@atuchin-m
Copy link
Collaborator

Does the PR cover all the heavy request processing or the other usage will be fixed separately?
E.g. https://github.com/brave/brave-core/blob/master/components/brave_news/browser/publishers_parsing.cc#L24 (is called during startup or other parsing things.

@atuchin-m
Copy link
Collaborator

Also it's not clear about FeedV2Builder lifetime and about safety of base::Unretained usage like this one:
https://github.com/brave/brave-core/blob/master/components/brave_news/browser/feed_v2_builder.cc#L934

After this change the callbacks start to be invoked async => if the object is destroyed we get UAF.

Copy link
Contributor

@sangwoo108 sangwoo108 left a comment

Choose a reason for hiding this comment

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

LGTM! @atuchin-m , I think this PR is safe now. Could you make the final call? I think we can apply scoped_ref partially on problematic data later, if copying cost turns out to be too expensive.


return feed;
FeedGenerationInfo info(
Copy link
Collaborator

@atuchin-m atuchin-m Feb 2, 2024

Choose a reason for hiding this comment

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

I wonder what it's the typical size of FeedGenerationInfo and how much CPU we need to construct it (including all the for-loops here)

Copy link
Contributor Author

@fallaciousreasoning fallaciousreasoning Feb 4, 2024

Choose a reason for hiding this comment

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

I imagine the typical size is large. 3500 FeedItems, similar number of TopicArticles, and maybe ~250 channels/publishers.

That said, I just timed this on my machine and it takes 11ms for this bit of code, which constructs the FeedGenerationInfo - not ideal, but not terrible either:
image

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sadly, 11ms also sounds like an issue for UI thread, especially for Android. UI thread is in charge of making frames each 33ms on average, therefore heavy computation is prohibited here.
From the upstream doc:

Do not perform expensive computation or blocking IO on the main thread
(a.k.a. “UI” thread in the browser process) or IO thread (each process's thread for receiving IPC).
A busy UI / IO thread can cause user-visible latency, so prefer running that work on the thread pool.

I believe the only way to solve this issue completely is to move all the inner things to a dedicated sequence (aka virtual thread in the pool).
A good example in Chromium code.
Otherwise, it's about impossible to manipulate such large structures on UI thread.

I'm not against solving the issue step-by-step, but I'd like to discuss and sync the final plan before we going to merge an individual PRs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah agreed, we should fix the root cause (that we're on the UI thread) rather than just putting bandaids over things. However, I think it's going to be a much more significant refactor to move everything into a sequence.

What I'm roughly thinking at the moment is this:

  1. We land & uplift this PR, roughly as is.
  2. We refactor BraveNewsController to have it's own sequence. Every call to subservices should happen on this sequence (this is going to be complicated because things are quite tightly tied to PrefService at the moment).
  3. We change this FeedGenerationInfo to just take a reference (still handy to tie everything together) and get rid of the base::ThreadPool usages here.

Does that sound reasonable? Happy to jump on a call to discuss 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, after a bit more research, I think something a bit like what PasswordStore does is probably the way to go.

We introduce a BraveNewsControllerFrontEnd. Lives on the UI thread, has a scoped_refptr to a BraveNewsController and SequencedTaskRunner which is posts tasks to the BraveNewsController on.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's merge this PR (after we sure that all the callback are good), this should improve the situation.
The next steps are to solve the issue completely. I hope I could provide you some perf metrics to track the progress (in #21876)

[](base::WeakPtr<FeedFetcher> fetcher, FetchFeedCallback callback,
std::tuple<FeedItems, ETags> result) {
// If we've been destroyed, don't run the callback.
if (!fetcher) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Checking fetcher makes sense. Although, the lifetime of the callback could be different.
Frankly, I can't 100% sure that we don't add new crashes because posting the callback.

The code use a very callback-based approach. It's really hard to surely find all the entry points and make a conclusion that it's still safe..

Copy link
Collaborator

@atuchin-m atuchin-m Feb 6, 2024

Choose a reason for hiding this comment

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

Im other words, we changed the public contract of the class. Before we run all the callbacks sync, now we could postpone. A callback could be bound not only to FeedFetcher but to a mojo-related interface (to send the answer) or something else. Better to double check all the callers. Or even make renaming DoSomething(..) => DoSomethingAsync(..)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we have changed the the public contract of the class - the callback was never invoked synchronously, only ever after all the combined feeds and direct feeds are fetched (quite apart from the network request, JSON parsing of the combined feed potentially happens in another process).

I added the check that fetcher wasn't destroyed because it was the only place I could think of where the contract was changing (previously the callback wouldn't be invoked if the FeedFetcher was destroyed, and the WeakPtr check maintains that guarantee).

Copy link
Contributor

github-actions bot commented Feb 6, 2024

[puLL-Merge] - brave/brave-core@21833

Description

This PR introduces several changes across multiple components of the brave-core repository, mainly focused on improving the handling and generation of news feeds in the Brave browser. The main motivation seems to be streamlining the consumption of news feeds by deduplicating content, improving asynchronous processing, and expanding the scope of what can be considered for inclusion in a news feed.

Changes

Changes

components/api_request_helper/api_request_helper.h

  • Added non-const overload for value_body() method in APIRequestResult class to allow modification of the json response as a base::Value.

components/brave_news/browser/DEPS

  • Included content/public/browser/browser_thread.h as a new dependency.

components/brave_news/browser/feed_fetcher.cc

  • Refactored feed fetching logic to utilize asynchronous task execution (base::ThreadPool::PostTaskAndReplyWithResult) both for parsing individual feed items and combining results from multiple sources. This change aims to improve performance by offloading work to a background thread and ensuring the UI thread remains responsive.
  • Introduced a new method CombineFeedSourceResults to deduplicate and combine feed items from various sources based on their URLs, improving the feed's overall quality by removing repeated articles.

components/brave_news/browser/feed_fetcher.h

  • Declaration of the newly introduced CombineFeedSourceResults method.

components/brave_news/browser/feed_v2_builder.cc

  • Added asynchronous task execution to GenerateBasicFeed and GenerateAllFeed methods, enhancing performance by moving heavy processing away from the UI thread. Besides, these methods now utilize more comprehensive data types and algorithms for assembling news feeds, including deduplication logic and dynamic content selection based on user subscriptions.
  • Introducing FeedGenerationInfo structure to pass comprehensive feed generation context between methods and across threads securely.
  • Updated method signatures and implementations to align with the asynchronous execution pattern and the use of FeedGenerationInfo.

components/brave_news/browser/feed_v2_builder.h

  • Introduced definition for FeedGenerationInfo struct and changed type aliases and method signatures to accommodate the new asynchronous feed generation pattern.

Security Hotspots

  1. Asynchronous Execution & Data Handling (components/brave_news/browser/feed_fetcher.cc, components/brave_news/browser/feed_v2_builder.cc): The introduction of asynchronous tasks and complex data manipulation increases the risk of race conditions and data inconsistency issues. Care should be taken to ensure that data accessed across threads is done safely and that any shared data structures are properly synchronized.

  2. URL Deduplication Logic (components/brave_news/browser/feed_fetcher.cc): The new logic to deduplicate URLs and combine feed items involves handling external input (URLs). It is vital to ensure that the URLs are validated and sanitized to prevent injection attacks or unintended access to local resources.

  3. Dependency on External Input (components/brave_news/browser/DEPS): Adding dependencies increases the project's attack surface, particularly if the dependencies are not well-maintained or have known vulnerabilities. Regular auditing of dependencies for security updates and vulnerabilities is recommended.

  4. Modification of JSON Response Data (components/api_request_helper/api_request_helper.h): The ability to modify the response data (base::Value) directly could lead to unintended side effects if the data is not correctly validated and sanitized, potentially leading to data corruption or exposure.

Copy link
Collaborator

@mkarolin mkarolin left a comment

Choose a reason for hiding this comment

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

DEPS++

// static
mojom::FeedV2Ptr FeedV2Builder::GenerateAllFeed(FeedGenerationInfo info) {
DVLOG(1) << __FUNCTION__;
DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
Copy link
Collaborator

Choose a reason for hiding this comment

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

it's not good to add content dependencies to components if they aren't strictly necessary because it makes this code unusable on ios

Copy link
Collaborator

Choose a reason for hiding this comment

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

It doesn't work here in a static method, but normally you can get the current sequence in the constructor and use that for sequence checks.

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

Successfully merging this pull request may close these issues.

[Perf] Fix JSON processing on UI thread
6 participants