[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.
Could these functions be renamed to not convey information about internal details? The main information that the listener needs to convey to this class are things like when executors start tasks, when the queue is idle, etc. These function names all are specific to internal timers, etc.

What bout having the external interface look like this:
```
def schedulerQueueBacklogged()
def schedulerQueueEmpty()
def executorIdle(executorId: String)
def executorBusy(executorId: String)
```