Completed
Pull Request — master (#1)
by Fabrice
06:06
created

FlowMap::getStats()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 23
c 0
b 0
f 0
rs 6.7272
cc 7
eloc 13
nc 13
nop 0
1
<?php
2
3
/*
4
 * This file is part of NodalFlow.
5
 *     (c) Fabrice de Stefanis / https://github.com/fab2s/NodalFlow
6
 * This source file is licensed under the MIT license which you will
7
 * find in the LICENSE file or at https://opensource.org/licenses/MIT
8
 */
9
10
namespace fab2s\NodalFlow\Flows;
11
12
use fab2s\NodalFlow\NodalFlowException;
13
use fab2s\NodalFlow\Nodes\AggregateNodeInterface;
14
use fab2s\NodalFlow\Nodes\BranchNodeInterface;
15
use fab2s\NodalFlow\Nodes\NodeInterface;
16
17
/**
18
 * class FlowMap
19
 * Do not implement Serializable interface on purpose
20
 *
21
 * @SEE https://externals.io/message/98834#98834
22
 */
23
class FlowMap implements FlowMapInterface
24
{
25
    /**
26
     * Flow map
27
     *
28
     * @var array
29
     */
30
    protected $nodeMap = [];
31
32
    /**
33
     * @var NodeInterface[]
34
     */
35
    protected $reverseMap = [];
36
37
    /**
38
     * The default Node Map values
39
     *
40
     * @var array
41
     */
42
    protected $nodeMapDefault = [
43
        'class'           => null,
44
        'flowId'          => null,
45
        'hash'            => null,
46
        'index'           => null,
47
        'isATraversable'  => null,
48
        'isAReturningVal' => null,
49
        'isAFlow'         => null,
50
    ];
51
52
    /**
53
     * The default Node stats values
54
     *
55
     * @var array
56
     */
57
    protected $nodeIncrements = [
58
        'num_exec'     => 0,
59
        'num_iterate'  => 0,
60
        'num_break'    => 0,
61
        'num_continue' => 0,
62
    ];
63
64
    /**
65
     * The Flow map default values
66
     *
67
     * @var array
68
     */
69
    protected $flowMapDefault = [
70
        'class'    => null,
71
        'id'       => null,
72
        'start'    => null,
73
        'end'      => null,
74
        'elapsed'  => null,
75
        'duration' => null,
76
        'mib'      => null,
77
    ];
78
79
    /**
80
     * @var array
81
     */
82
    protected $incrementTotals = [];
83
84
    /**
85
     * @var array
86
     */
87
    protected $flowIncrements;
88
89
    /**
90
     * @var array
91
     */
92
    protected $flowStats;
93
94
    /**
95
     * @var array
96
     */
97
    protected static $registry = [];
98
99
    /**
100
     * @var FlowInterface
101
     */
102
    protected $flow;
103
104
    /**
105
     * @var string
106
     */
107
    protected $flowId;
108
109
    /**
110
     * Instantiate a Flow Status
111
     *
112
     * @param FlowInterface $flow
113
     * @param array         $flowIncrements
114
     */
115
    public function __construct(FlowInterface $flow, array $flowIncrements = [])
116
    {
117
        $this->flow             = $flow;
118
        $this->flowId           = $this->flow->getId();
119
        $this->initDefaults()->setRefs()->setFlowIncrement($flowIncrements);
120
    }
121
122
    /**
123
     * The goal is to offer a cheap global state that supports
124
     * sending a parameter to whatever node in any live Flows.
125
     * We also need some kind of specific setup for each Flows
126
     * and Nodes (custom increments etc).
127
     *
128
     * The idea is to share a registry among all instances
129
     * without :
130
     *  - singleton: would only be useful to store the global state
131
     *      not the specific setup
132
     *  - breaking references: we want to be able to increment fast
133
     *      thus multiple entries at once by reference
134
     *  - DI: it would be a bit too much of code for the purpose and
135
     *      would come with the same limitation as the singleton
136
     *  - Complex and redundant merge strategies: registering a Node
137
     *      would then require to look up for ascendant and descendant
138
     *      Flows each time.
139
     *  - Breaking serialization: Playing with static and references
140
     *      require some attention. The matter is dealt with transparently
141
     *      as the static registry acts like a global cache feed with each
142
     *      instance data upon un-serialization.
143
     *
144
     * By using a static, we make sure all instances share the same
145
     * registry at all time in the simplest way.
146
     * Each FlowMap instance keeps dynamic reference to the portion
147
     * of the registry that belongs to the Flow holding the instance.
148
     * Upon Serialization, every FlowMap instance will thus only store
149
     * a portion of the global state, that is relevant to its carrying
150
     * Flow.
151
     *
152
     * Upon un-serialization, the global state is restored bit by bit
153
     * as each FlowMap gets un-serialized.
154
     * This leverage one edge case, that is, if we un-serialize two
155
     * time the same Flow (could only be members of different Flows).
156
     * This is not very likely as Flow id are immutable and unique,
157
     * but it could occur.
158
     * This situation is currently dealt with by throwing an exception,
159
     * as it would introduce the need to deal with distinct instances
160
     * with the same Id. And generating new ids is not simple either
161
     * as their whole point is to stay the same for others to know
162
     * who's who.
163
     * I find it a small trade of though, as un-serializing twice
164
     * the same string seems buggy and reusing the same Flow without
165
     * cloning is not such a good idea either (nor required).
166
     *
167
     * The use of a static registry also bring a somehow exotic feature :
168
     * The ability to target any Node in any Flow is not limited to the
169
     * flow within the same root Flow. You can actually target any Flow
170
     * in the current process. Using reference implementation would limit
171
     * the sharing to the root FLow members.
172
     * I don't know if this can be actually useful, but I don't think
173
     * it's such a big deal either.
174
     *
175
     * If you don't feel like doing this at home, I completely
176
     * understand, I'd be very happy to hear about a better and
177
     * more efficient way
178
     */
179
    public function __wakeup()
180
    {
181
        if (isset(static::$registry[$this->flowId])) {
182
            throw new NodalFlowException('Un-serializing a Flow when it is already instantiated is not supported', 1, null, [
183
                'class' => get_class($this->flow),
184
                'id'    => $this->flowId,
185
            ]);
186
        }
187
188
        $this->setRefs();
189
    }
190
191
    /**
192
     * @param NodeInterface $node
193
     * @param int           $index
194
     *
195
     * @throws NodalFlowException
196
     */
197
    public function register(NodeInterface $node, $index)
198
    {
199
        $this->enforceUniqueness($node);
200
        $nodeId                 = $node->getId();
201
        $this->nodeMap[$nodeId] = array_replace($this->nodeMapDefault, [
202
            'class'           => get_class($node),
203
            'flowId'          => $this->flowId,
204
            'hash'            => $nodeId,
205
            'index'           => $index,
206
            'isATraversable'  => $node->isTraversable(),
207
            'isAReturningVal' => $node->isReturningVal(),
208
            'isAFlow'         => $node->isFlow(),
209
        ], $this->nodeIncrements);
210
211
        $this->setNodeIncrement($node);
212
213
        if (isset($this->reverseMap[$index])) {
214
            // replacing a node, maintain nodeMap accordingly
215
            unset($this->nodeMap[$this->reverseMap[$index]->getId()]);
216
        }
217
218
        $this->reverseMap[$index] = $node;
219
    }
220
221
    /**
222
     * Triggered right before the flow starts
223
     *
224
     * @return $this
225
     */
226
    public function flowStart()
227
    {
228
        $this->flowStats['start'] = microtime(true);
229
230
        return $this;
231
    }
232
233
    /**
234
     * Triggered right after the flow stops
235
     *
236
     * @return $this
237
     */
238
    public function flowEnd()
239
    {
240
        $this->flowStats['end']     = microtime(true);
241
        $this->flowStats['mib']     = memory_get_peak_usage(true) / 1048576;
242
        $this->flowStats['elapsed'] = $this->flowStats['end'] - $this->flowStats['start'];
243
244
        $this->flowStats = array_replace($this->flowStats, $this->duration($this->flowStats['elapsed']));
245
246
        return $this;
247
    }
248
249
    /**
250
     * Let's be fast at incrementing while we are at it
251
     *
252
     * @param string $nodeHash
253
     *
254
     * @return array
255
     */
256
    public function &getNodeStat($nodeHash)
257
    {
258
        return $this->nodeMap[$nodeHash];
259
    }
260
261
    /**
262
     * Get/Generate Node Map
263
     *
264
     * @throws NodalFlowException
265
     *
266
     * @return array
267
     */
268
    public function getNodeMap()
269
    {
270
        foreach ($this->flow->getNodes() as $node) {
271
            $nodeId = $node->getId();
272
            if ($node instanceof BranchNodeInterface) {
273
                $this->nodeMap[$nodeId]['nodes'] = $node->getPayload()->getNodeMap();
274
                continue;
275
            }
276
277
            if ($node instanceof AggregateNodeInterface) {
278
                foreach ($node->getNodeCollection() as $aggregatedNode) {
279
                    $this->nodeMap[$nodeId]['nodes'][$aggregatedNode->getId()] = array_replace($this->nodeMapDefault, [
280
                        'class'  => get_class($aggregatedNode),
281
                        'flowId' => $this->flowId,
282
                        'hash'   => $aggregatedNode->getId(),
283
                    ]);
284
                }
285
                continue;
286
            }
287
        }
288
289
        return $this->nodeMap;
290
    }
291
292
    /**
293
     * Get the latest Node stats
294
     *
295
     * @return array
296
     */
297
    public function getStats()
298
    {
299
        foreach ($this->flow->getNodes() as $node) {
300
            $nodeMap = $this->nodeMap[$node->getId()];
301
            foreach ($this->incrementTotals as $srcKey => $totalKey) {
302
                if (isset($nodeMap[$srcKey])) {
303
                    $this->flowStats[$totalKey] += $nodeMap[$srcKey];
304
                }
305
            }
306
307
            if ($node instanceof BranchNodeInterface) {
308
                $childFlowId                               = $node->getPayload()->getId();
309
                $this->flowStats['branches'][$childFlowId] = $node->getPayload()->getStats();
310
                foreach ($this->incrementTotals as $srcKey => $totalKey) {
311
                    if (isset($this->flowStats['branches'][$childFlowId][$totalKey])) {
312
                        $this->flowStats[$totalKey] += $this->flowStats['branches'][$childFlowId][$totalKey];
313
                    }
314
                }
315
            }
316
        }
317
318
        return $this->flowStats;
319
    }
320
321
    /**
322
     * @param string $nodeHash
323
     * @param string $key
324
     *
325
     * @return $this
326
     */
327
    public function incrementNode($nodeHash, $key)
328
    {
329
        ++$this->nodeMap[$nodeHash][$key];
330
331
        return $this;
332
    }
333
334
    /**
335
     * @param string $key
336
     *
337
     * @return $this
338
     */
339
    public function incrementFlow($key)
340
    {
341
        ++$this->flowStats[$key];
342
343
        return $this;
344
    }
345
346
    /**
347
     * Resets Nodes stats, can be used prior to Flow's re-exec
348
     *
349
     * @return $this
350
     */
351
    public function resetNodeStats()
352
    {
353
        foreach ($this->nodeMap as &$nodeStat) {
354
            foreach ($this->nodeIncrements as $key => $value) {
355
                if (isset($nodeStat[$key])) {
356
                    $nodeStat[$key] = $value;
357
                }
358
            }
359
        }
360
361
        return $this;
362
    }
363
364
    /**
365
     * Computes a human readable duration string from floating seconds
366
     *
367
     * @param float $seconds
368
     *
369
     * @return array<string,integer|string>
370
     */
371
    public function duration($seconds)
372
    {
373
        $result = [
374
            'hour'     => (int) floor($seconds / 3600),
375
            'min'      => (int) floor(($seconds / 60) % 60),
376
            'sec'      => $seconds % 60,
377
            'ms'       => (int) round(\fmod($seconds, 1) * 1000),
378
        ];
379
380
        $duration = '';
381
        foreach ($result as $unit => $value) {
382
            if (!empty($value) || $unit === 'ms') {
383
                $duration .= $value . "$unit ";
384
            }
385
        }
386
387
        $result['duration'] = trim($duration);
388
389
        return $result;
390
    }
391
392
    /**
393
     * @return $this
394
     */
395
    protected function setRefs()
396
    {
397
        static::$registry[$this->flowId]['flowStats'] = &$this->flowStats;
398
        static::$registry[$this->flowId]['nodeStats'] = &$this->nodeMap;
399
        static::$registry[$this->flowId]['flow']      = $this->flow;
400
        static::$registry[$this->flowId]['nodes']     = &$this->reverseMap;
401
402
        return $this;
403
    }
404
405
    /**
406
     * @return $this
407
     */
408
    protected function initDefaults()
409
    {
410
        $this->flowIncrements = $this->nodeIncrements;
411
        foreach (array_keys($this->flowIncrements) as $key) {
412
            $totalKey                        = $key . '_total';
413
            $this->incrementTotals[$key]     = $totalKey;
414
            $this->flowIncrements[$totalKey] = 0;
415
        }
416
417
        $this->flowMapDefault = array_replace($this->flowMapDefault, $this->flowIncrements, [
418
            'class' => get_class($this->flow),
419
            'id'    => $this->flowId,
420
        ]);
421
422
        $this->flowStats = $this->flowMapDefault;
423
424
        return $this;
425
    }
426
427
    /**
428
     * Set additional increment keys, use :
429
     *      'keyName' => int
430
     * to add keyName as increment, starting at int
431
     * or :
432
     *      'keyName' => 'existingIncrement'
433
     * to assign keyName as a reference to existingIncrement
434
     *
435
     * @param array $flowIncrements
436
     *
437
     * @throws NodalFlowException
438
     *
439
     * @return $this
440
     */
441
    protected function setFlowIncrement(array $flowIncrements)
442
    {
443
        foreach ($flowIncrements as $incrementKey => $target) {
444
            if (is_string($target)) {
445
                if (!isset($this->flowStats[$target])) {
446
                    throw new NodalFlowException('Cannot set reference on unset target');
447
                }
448
449
                if (substr($incrementKey, -6) === '_total') {
450
                    $this->incrementTotals[$incrementKey] = $target;
451
                    $this->flowStats[$incrementKey]       = 0;
452
                    continue;
453
                }
454
455
                $this->flowStats[$incrementKey] = &$this->flowStats[$target];
456
                continue;
457
            }
458
459
            $this->flowIncrements[$incrementKey] = $target;
460
            $this->flowStats[$incrementKey]      = $target;
461
        }
462
463
        return $this;
464
    }
465
466
    /**
467
     * @param NodeInterface $node
468
     *
469
     * @throws NodalFlowException
470
     *
471
     * @return $this
472
     */
473
    protected function setNodeIncrement(NodeInterface $node)
474
    {
475
        $nodeId = $node->getId();
476
        foreach ($node->getNodeIncrements() as $incrementKey => $target) {
477
            if (is_string($target)) {
478
                if (!isset($this->nodeIncrements[$target])) {
479
                    throw new NodalFlowException('Tried to set an increment alias to an un-registered increment', 1, null, [
480
                        'aliasKey'  => $incrementKey,
481
                        'targetKey' => $target,
482
                    ]);
483
                }
484
485
                $this->nodeMap[$nodeId][$incrementKey] = &$this->nodeMap[$nodeId][$target];
486
                continue;
487
            }
488
489
            $this->nodeIncrements[$incrementKey]   = $target;
490
            $this->nodeMap[$nodeId][$incrementKey] = $target;
491
        }
492
493
        return $this;
494
    }
495
496
    /**
497
     * @param NodeInterface $node
498
     *
499
     * @throws NodalFlowException
500
     *
501
     * @return $this
502
     */
503
    protected function enforceUniqueness(NodeInterface $node)
504
    {
505
        if (isset($this->nodeMap[$node->getId()])) {
506
            throw new NodalFlowException('Cannot reuse Node instances within a Flow', 1, null, [
507
                'duplicate_node' => get_class($node),
508
                'hash'           => $node->getId(),
509
            ]);
510
        }
511
512
        return $this;
513
    }
514
}
515