Repository: twisted/twisted

Issue:
Speed up Deferred: isinstance() is slow
Deferred callback does an `instance(result, Deferred)` in the hot path, which is a pretty expensive operation.

Discussion:

Patch:
diff --git a/src/twisted/internet/defer.py b/src/twisted/internet/defer.py
index 09aea86142..46696d61dc 100644
--- a/src/twisted/internet/defer.py
+++ b/src/twisted/internet/defer.py
@@ -1090,30 +1090,33 @@ class Deferred(Awaitable[_SelfResultT]):
                     # expensive, so we avoid it unless self.debug is set.
                     current.result = Failure(captureVars=self.debug)
                 else:
-                    if isinstance(current.result, Deferred):
+                    # isinstance() with Awaitable subclass is expensive:
+                    if type(current.result) in _DEFERRED_SUBCLASSES:
+                        # Can't use cast() cause it's in the performance hot path:
+                        currentResult: Deferred[_SelfResultT] = current.result  # type: ignore[assignment]
                         # The result is another Deferred.  If it has a result,
                         # we can take it and keep going.
-                        resultResult = getattr(current.result, "result", _NO_RESULT)
+                        resultResult = getattr(currentResult, "result", _NO_RESULT)
                         if (
                             resultResult is _NO_RESULT
-                            or isinstance(resultResult, Deferred)
-                            or current.result.paused
+                            or type(resultResult) in _DEFERRED_SUBCLASSES
+                            or currentResult.paused
                         ):
                             # Nope, it didn't.  Pause and chain.
                             current.pause()
-                            current._chainedTo = current.result
+                            current._chainedTo = currentResult
                             # Note: current.result has no result, so it's not
                             # running its callbacks right now.  Therefore we can
                             # append to the callbacks list directly instead of
                             # using addCallbacks.
-                            current.result.callbacks.append(current._continuation())
+                            currentResult.callbacks.append(current._continuation())
                             break
                         else:
                             # Yep, it did.  Steal it.
-                            current.result.result = None
+                            currentResult.result = None
                             # Make sure _debugInfo's failure state is updated.
-                            if current.result._debugInfo is not None:
-                                current.result._debugInfo.failResult = None
+                            if currentResult._debugInfo is not None:
+                                currentResult._debugInfo.failResult = None
                             current.result = resultResult
 
             if finished:
@@ -1322,6 +1325,14 @@ class Deferred(Awaitable[_SelfResultT]):
             return _cancellableInlineCallbacks(coro)
         raise NotACoroutineError(f"{coro!r} is not a coroutine")
 
+    def __init_subclass__(cls: Type[Deferred[Any]], **kwargs: Any):
+        # Whenever a subclass is created, record it in L{_DEFERRED_SUBCLASSES}
+        # so we can emulate C{isinstance()} more efficiently.
+        _DEFERRED_SUBCLASSES.append(cls)
+
+
+_DEFERRED_SUBCLASSES = [Deferred]
+
 
 def ensureDeferred(
     coro: Union[
diff --git a/src/twisted/newsfragments/12223.feature b/src/twisted/newsfragments/12223.feature
new file mode 100644
index 0000000000..78991647ac
--- /dev/null
+++ b/src/twisted/newsfragments/12223.feature
@@ -0,0 +1,1 @@
+twisted.internet.defer.Deferred.addCallback now runs about 10% faster.
\ No newline at end of file


Test patch:
diff --git a/benchmarks/test_deferred.py b/benchmarks/test_deferred.py
index f2d9e03d56..2474e10819 100644
--- a/benchmarks/test_deferred.py
+++ b/benchmarks/test_deferred.py
@@ -78,3 +78,53 @@ def test_deferred_errback_chain(benchmark):
         d.addBoth(swallowErr)
 
     benchmark(go)
+
+
+def test_deferred_chained_already_fired(benchmark):
+    """
+    Measure speed of chained Deferred, where the chained Deferred fires before
+    the callback returning it is added.
+    """
+
+    def go():
+        d = Deferred()
+        d2 = Deferred()
+        d3 = Deferred()
+        d2.callback(123)
+        d3.callback(456)
+        d.addCallback(lambda _: d2)
+        d.addCallback(lambda _: d3)
+        d.callback(123)
+        return d
+
+    d = benchmark(go)
+    result = []
+    d.addCallback(result.append)
+    assert result == [456]
+
+
+def test_deferred_chained_not_fired(benchmark):
+    """
+    Measure speed of chained Deferred, where the chained Deferred fires after
+    the callback returning it is added.
+    """
+
+    def go():
+        d = Deferred()
+        d2 = Deferred()
+        # d3 has its own chained result:
+        d3 = Deferred()
+        d4 = Deferred()
+        d3.addCallback(lambda _: d4)
+        d3.callback(123)
+        d.addCallback(lambda _: d2)
+        d.addCallback(lambda _: d3)
+        d2.callback(123)
+        d4.callback(57)
+        d.callback(7)
+        return d
+
+    d = benchmark(go)
+    result = []
+    d.addCallback(result.append)
+    assert result == [57]
diff --git a/src/twisted/test/test_defer.py b/src/twisted/test/test_defer.py
index 9302a2a16d..8f8ebd5737 100644
--- a/src/twisted/test/test_defer.py
+++ b/src/twisted/test/test_defer.py
@@ -1811,6 +1811,23 @@ class DeferredTests(unittest.SynchronousTestCase, ImmediateFailureMixin):
         # been freed.
         self.assertIsNone(weakCanceller())
 
+    def test_DEFERRED_SUBCLASSES(self) -> None:
+        """
+        C{_DEFERRED_SUBCLASSES} includes all subclasses of L{Deferred}.
+        """
+        self.assertEqual(defer._DEFERRED_SUBCLASSES[:2], [Deferred, DeferredList])
+
+        class D2(Deferred[int]):
+            pass
+
+        self.assertEqual(defer._DEFERRED_SUBCLASSES[-1], D2)
+        defer._DEFERRED_SUBCLASSES.pop(-1)
+
+        # There are other potential subclasses, e.g. twisted.persisted.crefutil
+        # has a Deferred subclass:
+        for klass in defer._DEFERRED_SUBCLASSES:
+            self.assertTrue(issubclass(klass, Deferred))
+
 
 class DummyCanceller:
     """