-
Notifications
You must be signed in to change notification settings - Fork 604
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
[node-core-library][rush] Support weighted async concurrency #4092
Conversation
a1b0f7d
to
e24b290
Compare
@@ -33,7 +33,9 @@ export class AnsiEscape { | |||
|
|||
// @beta | |||
export class Async { | |||
static forEachAsync<TEntry>(iterable: Iterable<TEntry> | AsyncIterable<TEntry>, callback: (entry: TEntry, arrayIndex: number) => Promise<void>, options?: IAsyncParallelismOptions | undefined): Promise<void>; | |||
static forEachAsync<TEntry>(iterable: Iterable<TEntry> | AsyncIterable<TEntry>, callback: (entry: TEntry, arrayIndex: number) => Promise<void>, options?: TEntry extends { | |||
weight?: number; |
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.
This type algebra seems overcomplicated. I think is trying to say that IF the options specifies useWeight
, THEN the entry must define a weight
. I don't think it actually accomplishes that, but it is a slightly odd requirement given that both these properties are optional. If weight defaults to 1
, then is it really important to have been able to specify it, even though we chose not to?
I think it might be better to simply say that "If useWeight
=true, then forEachSync
will use the weight
property if present, otherwise the weight
defaults to 1
." The only realistic concern here is if old code randomly had a weight
property, that suddenly gains new meaning in this release, but you've already solved that by defaulting useWeight
to false.
This would also eliminate the need for a separate IAsyncParallelismOptionsWithWeight
interface.
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.
The check on the optional property is to ensure that the TEntry
type does not have a property weight
that is incompatible with type number
. I originally considered writing this as an overload, but I'm generally leery of overloads in case something uses typeof Async.forEachAsync
somewhere. Might be cleanest to just add a completely separate entry point.
*/ | ||
export interface IAsyncParallelismOptionsWithWeight extends IAsyncParallelismOptions { | ||
/** | ||
* If set, then will use `item.weight` instead of `1` as the parallelism consumed by a given iteration. |
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.
This needs more explanation. I had to read the code carefully to figure out what weight
actually represents. My guess was that somehow affected the priority of the task (like Unix nice
), but that was wrong. Also, the PR description and code comments never mention why someone would want this feature, i.e. what problem it solves.
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.
Try something like this:
/**
* Optionally used with the {@link Async.mapAsync} and {@link Async.forEachAsync}
* to limit amount of concurrency.
* @remarks
* By default, `concurrency` simply specifies the maximum number of promises that can be active
* simultaneously. However if `useWeight` is true, then behavior
* is different. See the {@link IAsyncParallelismOptions.useWeight} documentation for details.
*/
concurrency?: number;
/**
* Set `useWeight` to true to configure the {@link IAsyncParallelismOptions.concurrency}
* setting to limit the total resource cost instead of limiting the total number of promises.
* @remarks
* By default, the `concurrency` specifies the maximum number of promises that can be
* active simultaneously. If promises have different resource costs, and those costs are known
* in advance, then `useWeight` changes `concurrency` so that it limits the sum of the weights
* of the entries passed to {@link Async.mapAsync} or {@link Async.forEachAsync}.
*
* The {@link IAsyncEntry.weight} property is optional and defaults to 1. (If all entries
* use the default of 1, then the behavior is identical to `useWeight=false`.)
* Weights can be fractional but not negative.
*/
useWeight?: boolean;
/**
* If {@link Async.mapAsync} or {@link Async.forEachAsync} are used to parallelize tasks
* that have different resource costs, and this property can be used to specify the resource
* cost so that it can be limited by {@link IAsyncParallelismOptions.concurrency}.
* @remarks
* This property is ignored unless `useWeight` is true. See the {@link
* IAsyncParallelismOptions.useWeight} documentation for details.
*/
weight?: number
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.
Is weight
the best word? Maybe concurrencyResourceCost
(or at least concurrencyWeight
)? It would be great of the naming gave some clue that concurrency
and weight
are part of the same feature.
Also we are asking to assign weight
directly to somebody else's class/interface, so the more unique this name is, the less likely it is to accidentally conflict with an unrelated member.
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.
The name weight
was chosen purely to match its existing presence on Rush's Operation
class, but that could be changed to whatever name seems to make the most sense.
I'm also open to having an altogether separate entry point for tasks whose CPU resource needs are not uniformly 1.
@@ -33,7 +33,9 @@ export class AnsiEscape { | |||
|
|||
// @beta | |||
export class Async { | |||
static forEachAsync<TEntry>(iterable: Iterable<TEntry> | AsyncIterable<TEntry>, callback: (entry: TEntry, arrayIndex: number) => Promise<void>, options?: IAsyncParallelismOptions | undefined): Promise<void>; | |||
static forEachAsync<TEntry>(iterable: Iterable<TEntry> | AsyncIterable<TEntry>, callback: (entry: TEntry, arrayIndex: number) => Promise<void>, options?: TEntry extends { |
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.
{ weight?: number }
must be declared as an interface, so that we have some place to document the weight
property.
@@ -319,6 +321,13 @@ export interface IAsyncParallelismOptions { | |||
concurrency?: number; | |||
} | |||
|
|||
// Warning: (ae-incompatible-release-tags) The symbol "IAsyncParallelismOptionsWithWeight" is marked as @public, but its signature references "IAsyncParallelismOptions" which is marked as @beta |
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.
Fix this?
I'm going to rework this PR with a different mechanism for addressing the same end goal. |
Summary
Updates
Async.forEachAsync
with the ability to have variable concurrency based on task weights.Updates Rush to use the feature for phased build concurrency.
The motivation for this feature is that when distributing tasks across available CPUs, sometimes a task may only use the target CPU some of the time (e.g. a network request) and therefore contributes a value
<1
to the overall concurrency limit, and conversely sometimes a task may spin up multiple CPU-intensive threads and therefore contributes a value>1
to the overall concurrency limit.Details
Adds a new option
useWeight: true
toAsync.forEachAsync
that is only available when the item type has a propertyweight?: number
. If set, this value gets used for comparing available concurrency, instead of the default1
.How it was tested
Added unit test for the scenario.
Impacted documentation
Just the docs for the method.