Skip to content

Commit 00c4299

Browse files
authored
Use array lookup instead of recursion in Context (#821)
* Use array instead of recursion in `Context` * Move span and baggage context key to context package * Add additional context tests
1 parent fc0a6d6 commit 00c4299

File tree

2 files changed

+91
-105
lines changed

2 files changed

+91
-105
lines changed

Context.php

Lines changed: 66 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -4,94 +4,68 @@
44

55
namespace OpenTelemetry\Context;
66

7-
use function assert;
7+
use function spl_object_id;
88

99
/**
10-
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.6.1/specification/context/context.md#overview
10+
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/README.md#context
1111
*/
1212
final class Context
1313
{
14-
/**
15-
* @var ContextStorageInterface&ExecutionContextAwareInterface
16-
*/
14+
/** @var ContextStorageInterface&ExecutionContextAwareInterface */
1715
private static ContextStorageInterface $storage;
18-
private static ?self $root = null;
19-
private ?ContextKey $key;
20-
/**
21-
* @var mixed
22-
*/
23-
private $value;
24-
private ?self $parent;
2516

26-
/**
27-
* This is a general purpose read-only key-value store. Read-only in the sense that adding a new value does not
28-
* mutate the existing context, but returns a new Context which has the new value added.
29-
*
30-
* In practical terms, this is implemented as a linked list of Context instances, with each one holding a reference
31-
* to the key object, the value that corresponds to the key, and an optional reference to the parent Context
32-
* (i.e. the next link in the linked list chain)
33-
*
34-
* @param ContextKey|null $key The key object. Should only be null when creating an "empty" context
35-
* @param mixed $value
36-
* @param Context|null $parent Reference to the parent object
37-
*/
38-
private function __construct(?ContextKey $key=null, $value=null, ?self $parent=null)
17+
// Optimization for spans to avoid copying the context array.
18+
private static ContextKey $spanContextKey;
19+
private ?object $span = null;
20+
/** @var array<int, mixed> */
21+
private array $context = [];
22+
/** @var array<int, ContextKey> */
23+
private array $contextKeys = [];
24+
25+
private function __construct()
3926
{
40-
$this->key = $key;
41-
$this->value = $value;
42-
$this->parent = $parent;
27+
self::$spanContextKey = ContextKeys::span();
4328
}
4429

45-
/**
46-
* Static version of get()
47-
* This is primarily useful when the caller doesn't already have a reference to the Context that they want to mutate.
48-
* This will operate on the "current" global context in that scenario.
49-
*
50-
* There are two ways to call this function:
51-
* 1) With a $context value:
52-
* Context::getValue($key, $context) is functionally equivalent to $context->get($key)
53-
* 2) Without a $context value:
54-
* This will fetch the "current" Context if one exists or create one if not, then attempt to get the value from it.
55-
*
56-
* @param ContextKey $key
57-
* @param Context|null $context
58-
*
59-
* @return mixed
60-
*/
61-
public static function getValue(ContextKey $key, ?self $context=null)
30+
public static function createKey(string $key): ContextKey
6231
{
63-
$context ??= self::getCurrent();
64-
65-
return $context->get($key);
32+
return new ContextKey($key);
6633
}
6734

6835
/**
69-
* This adds a key/value pair to this Context. We do this by instantiating a new Context instance with the key/value and pass
70-
* a reference to $this as the "parent" creating the linked list chain.
71-
*
72-
* @param ContextKey $key
73-
* @param mixed $value
36+
* @internal
7437
*
75-
* @return Context a new Context containing the key/value
38+
* @param ContextStorageInterface&ExecutionContextAwareInterface $storage
7639
*/
77-
public function with(ContextKey $key, $value): self
40+
public static function setStorage(ContextStorageInterface $storage): void
7841
{
79-
return new self($key, $value, $this);
42+
self::$storage = $storage;
8043
}
8144

8245
/**
83-
* @todo: Implement this on the API side
46+
* @return ContextStorageInterface&ExecutionContextAwareInterface
8447
*/
85-
public function withContextValue(ImplicitContextKeyedInterface $value): self
48+
public static function storage(): ContextStorageInterface
8649
{
87-
return $value->storeInContext($this);
50+
/** @psalm-suppress RedundantPropertyInitializationCheck */
51+
return self::$storage ??= new ContextStorage();
8852
}
8953

9054
/**
91-
* Makes `$this` the currently active {@see Context}.
92-
*
93-
* @todo: Implement this on the API side
55+
* @internal
9456
*/
57+
public static function getRoot(): Context
58+
{
59+
static $empty;
60+
61+
return $empty ??= new self();
62+
}
63+
64+
public static function getCurrent(): Context
65+
{
66+
return self::storage()->current();
67+
}
68+
9569
public function activate(): ScopeInterface
9670
{
9771
$scope = self::storage()->attach($this);
@@ -101,58 +75,45 @@ public function activate(): ScopeInterface
10175
return $scope;
10276
}
10377

104-
/**
105-
* Fetch a value from the Context given a key value.
106-
*
107-
* @return mixed
108-
*/
109-
public function get(ContextKey $key)
78+
public function withContextValue(ImplicitContextKeyedInterface $value): Context
11079
{
111-
for ($context = $this; $context; $context = $context->parent) {
112-
if ($context->key === $key) {
113-
return $context->value;
114-
}
115-
}
116-
117-
return null;
80+
return $value->storeInContext($this);
11881
}
11982

120-
/**
121-
* @internal
122-
*
123-
* @param ContextStorageInterface&ExecutionContextAwareInterface $storage
124-
*/
125-
public static function setStorage(ContextStorageInterface $storage): void
83+
public function with(ContextKey $key, $value): self
12684
{
127-
self::$storage = $storage;
128-
}
85+
if ($this->get($key) === $value) {
86+
return $this;
87+
}
12988

130-
/**
131-
* @return ContextStorageInterface&ExecutionContextAwareInterface
132-
*/
133-
public static function storage(): ContextStorageInterface
134-
{
135-
/** @psalm-suppress RedundantPropertyInitializationCheck */
136-
return self::$storage ??= new ContextStorage();
137-
}
89+
$self = clone $this;
13890

139-
/**
140-
* @param non-empty-string $key
141-
*
142-
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.6.1/specification/context/context.md#create-a-key
143-
*/
144-
public static function createKey(string $key): ContextKey
145-
{
146-
return new ContextKey($key);
147-
}
91+
if ($key === self::$spanContextKey) {
92+
$self->span = $value; // @phan-suppress-current-line PhanTypeMismatchPropertyReal
14893

149-
public static function getCurrent(): self
150-
{
151-
return self::storage()->current();
94+
return $self;
95+
}
96+
97+
$id = spl_object_id($key);
98+
if ($value !== null) {
99+
$self->context[$id] = $value;
100+
$self->contextKeys[$id] ??= $key;
101+
} else {
102+
unset(
103+
$self->context[$id],
104+
$self->contextKeys[$id],
105+
);
106+
}
107+
108+
return $self;
152109
}
153110

154-
public static function getRoot(): self
111+
public function get(ContextKey $key)
155112
{
156-
return self::$root ??= new self();
113+
if ($key === self::$spanContextKey) {
114+
return $this->span;
115+
}
116+
117+
return $this->context[spl_object_id($key)] ?? null;
157118
}
158119
}

ContextKeys.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Context;
6+
7+
/**
8+
* @psalm-internal OpenTelemetry
9+
*/
10+
final class ContextKeys
11+
{
12+
public static function span(): ContextKey
13+
{
14+
static $instance;
15+
16+
return $instance ??= Context::createKey('opentelemetry-trace-span-key');
17+
}
18+
19+
public static function baggage(): ContextKey
20+
{
21+
static $instance;
22+
23+
return $instance ??= Context::createKey('opentelemetry-trace-baggage-key');
24+
}
25+
}

0 commit comments

Comments
 (0)