[SPARK-3795] Heuristics for dynamically scaling executors

This is part of a bigger effort to provide elastic scaling of executors within a Spark application ([SPARK-3174](https://issues.apache.org/jira/browse/SPARK-3174)). This PR does not provide any functionality by itself; it is a skeleton that is missing a mechanism to be added later in [SPARK-3822](https://issues.apache.org/jira/browse/SPARK-3822).

Comments and feedback are most welcome. For those of you reviewing this in detail, I highly recommend doing it through your favorite IDE instead of through the diff here.
I don't think it's correct here to assume that if a stage has N tasks that it has tasks of index (0 to N-1). In some cases a stage can be submitted that computes only some indices of an RDD. I think in this case it is sufficient to just keep track of numTasks for each stage and then keep track of the number of distinct taskIndices that have completed. Once the size of that set == numTasks, you can assume it's fine. One other question is how this works with speculation... it would be good to look at the code and understand how speculative tasks are submitted to the listener (I think right now there is no way for the listener to know about them) /cc @kayousterhout.

It might be fine to simply ignore these for now when computing the backlog. However, we need to make sure there is no corner case where a task is submitted according to this Listener but we never clear it. Otherwise we might constantly think we are backlogged.