2015-12-20 5 views
15

Spesso non è molto chiaro in che modo esattamente il flusso parallelo divide l'input in blocchi e in quale ordine vengono uniti i blocchi. C'è un modo per visualizzare l'intera procedura per qualsiasi fonte di streaming per capire meglio cosa sta succedendo? Supponiamo che ho creato un flusso simile a questo:Visualizzazione della parallelizzazione di Java Stream

Voglio vedere qualche albero-come la struttura:

   [0..99] 
     _____/ \_____ 
     |    | 
    [0..49]   [50..99] 
    __/ \__  __/ \__ 
    |   |  |  | 
[0..24] [25..49] [50..74] [75..99] 

Il che significa che l'intero campo di ingresso [0..99] è diviso per [0..49] e [50..99] gamme che a sua volta diviso ulteriormente. Ovviamente tale diagramma dovrebbe riflettere il vero lavoro dell'API Stream, quindi se eseguo qualche operazione reale con tale stream la suddivisione dovrebbe essere eseguita nello stesso modo.

risposta

8

voglio aumentare Tagir’s great answer con una soluzione monitorare la scissione a lato sorgente o anche a operazioni intermedie (con alcune limitazioni imposte dalla attuazione flusso attuale delle API):

public static <E> Stream<E> proxy(Stream<E> src) { 
    Class<Stream<E>> sClass=(Class)Stream.class; 
    Class<Spliterator<E>> spClass=(Class)Spliterator.class; 
    return proxy(src, sClass, spClass, StreamSupport::stream); 
} 
public static IntStream proxy(IntStream src) { 
    return proxy(src, IntStream.class, Spliterator.OfInt.class, StreamSupport::intStream); 
} 
public static LongStream proxy(LongStream src) { 
    return proxy(src, LongStream.class, Spliterator.OfLong.class, StreamSupport::longStream); 
} 
public static DoubleStream proxy(DoubleStream src) { 
    return proxy(src, DoubleStream.class, Spliterator.OfDouble.class, StreamSupport::doubleStream); 
} 
static final Object EMPTY=new StringBuilder("empty"); 
static <E,S extends BaseStream<E,S>, Sp extends Spliterator<E>> S proxy(
     S src, Class<S> sc, Class<Sp> spc, BiFunction<Sp,Boolean,S> f) { 

    final class Node<T> implements InvocationHandler,Runnable, 
     Consumer<Object>, IntConsumer, LongConsumer, DoubleConsumer { 
     final Class<? extends Spliterator> type; 
     Spliterator<T> src; 
     Object first=EMPTY, last=EMPTY; 
     Node<T> left, right; 
     Object currConsumer; 
     public Node(Spliterator<T> src, Class<? extends Spliterator> type) { 
      this.src = src; 
      this.type=type; 
     } 
     private void value(Object t) { 
      if(first==EMPTY) first=t; 
      last=t; 
     } 
     public void accept(Object t) { 
      value(t); ((Consumer)currConsumer).accept(t); 
     } 
     public void accept(int t) { 
      value(t); ((IntConsumer)currConsumer).accept(t); 
     } 
     public void accept(long t) { 
      value(t); ((LongConsumer)currConsumer).accept(t); 
     } 
     public void accept(double t) { 
      value(t); ((DoubleConsumer)currConsumer).accept(t); 
     } 
     public void run() { 
      System.out.println(); 
      finish().forEach(System.out::println); 
     } 
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
      Node<T> curr=this; while(curr.right!=null) curr=curr.right; 
      if(method.getName().equals("tryAdvance")||method.getName().equals("forEachRemaining")) { 
       curr.currConsumer=args[0]; 
       args[0]=curr; 
      } 
      if(method.getName().equals("trySplit")) { 
       Spliterator s=curr.src.trySplit(); 
       if(s==null) return null; 
       Node<T> pfx=new Node<>(s, type); 
       pfx.left=curr.left; curr.left=pfx; 
       curr.right=new Node<>(curr.src, type); 
       src=null; 
       return pfx.create(); 
      } 
      return method.invoke(curr.src, args); 
     } 
     Object create() { 
      return Proxy.newProxyInstance(null, new Class<?>[]{type}, this); 
     } 
     String pad(String s, int left, int len) { 
      if (len == s.length()) 
       return s; 
      char[] result = new char[len]; 
      Arrays.fill(result, ' '); 
      s.getChars(0, s.length(), result, left); 
      return new String(result); 
     } 
     public List<String> finish() { 
      String cur = toString(); 
      if (left == null) { 
       return Collections.singletonList(cur); 
      } 
      List<String> l = left.finish(); 
      List<String> r = right.finish(); 
      int len1 = l.get(0).length(); 
      int len2 = r.get(0).length(); 
      int totalLen = len1 + len2 + 1; 
      int leftAdd = 0; 
      if (cur.length() < totalLen) { 
       cur = pad(cur, (totalLen - cur.length())/2, totalLen); 
      } else { 
       leftAdd = (cur.length() - totalLen)/2; 
       totalLen = cur.length(); 
      } 
      List<String> result = new ArrayList<>(); 
      result.add(cur); 

      char[] dashes = new char[totalLen]; 
      Arrays.fill(dashes, ' '); 
      Arrays.fill(dashes, len1/2 + leftAdd + 1, len1 + len2/2 + 1 
        + leftAdd, '_'); 
      int mid = totalLen/2; 
      dashes[mid] = '/'; 
      dashes[mid + 1] = '\\'; 
      result.add(new String(dashes)); 

      Arrays.fill(dashes, ' '); 
      dashes[len1/2 + leftAdd] = '|'; 
      dashes[len1 + len2/2 + 1 + leftAdd] = '|'; 
      result.add(new String(dashes)); 

      int maxSize = Math.max(l.size(), r.size()); 
      for (int i = 0; i < maxSize; i++) { 
       String lstr = l.size() > i ? l.get(i) : String.format("%" 
         + len1 + "s", ""); 
       String rstr = r.size() > i ? r.get(i) : String.format("%" 
         + len2 + "s", ""); 
       result.add(pad(lstr + " " + rstr, leftAdd, totalLen)); 
      } 
      return result; 
     } 
     private Object first() { 
      if(left==null) return first; 
      Object o=left.first(); 
      if(o==EMPTY) o=right.first(); 
      return o; 
     } 
     private Object last() { 
      if(right==null) return last; 
      Object o=right.last(); 
      if(o==EMPTY) o=left.last(); 
      return o; 
     } 
     public String toString() { 
      Object o=first(), p=last(); 
      return o==EMPTY? "(empty)": "["+o+(o!=p? ".."+p+']': "]"); 
     } 
    } 
    Node<E> n=new Node<>(src.spliterator(), spc); 
    Sp sp=(Sp)Proxy.newProxyInstance(null, new Class<?>[]{n.type}, n); 
    return f.apply(sp, true).onClose(n); 
} 

Permette di avvolgere uno spliterator con un proxy che monitorerà le operazioni di divisione e gli oggetti incontrati. La logica della gestione del blocco è simile a quella di Tagir, infatti, ho copiato le sue routine di stampa dei risultati.

È possibile passare la sorgente del flusso o di un flusso con le stesse operazioni già aggiunte. (In quest'ultimo caso, è necessario applicare .parallel() il prima possibile allo stream). Come spiegato Tagir, nella maggior parte dei casi, il comportamento divisione dipende dalla sorgente e il parallelismo configurato, quindi, nella maggioranza dei casi, il monitoraggio stati intermedi possono cambiare i valori, ma non i pezzi lavorati:

try(IntStream is=proxy(IntStream.range(0, 100).parallel())) { 
    is.filter(i -> i/20%2==0) 
     .mapToObj(ix->"\""+ix+'"') 
     .forEach(s->{}); 
} 

stamperà

                [0..99]                 
            ___________________________________/\________________________________          
            |                  |         
           [0..49]                [50..99]         
       _________________/\______________          _________________/\________________     
       |         |         |         |     
      [0..24]       [25..49]       [50..74]       [75..99]    
     ________/\_____     ________/\_______     ________/\_______     ________/\_______   
     |    |     |     |     |     |     |     |   
    [0..11]   [12..24]   [25..36]   [37..49]   [50..61]   [62..74]   [75..86]   [87..99]  
    ___/\_   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___  
    |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | 
[0..5] [6..11] [12..17] [18..24] [25..30] [31..36] [37..42] [43..49] [50..55] [56..61] [62..67] [68..74] [75..80] [81..86] [87..92] [93..99] 

mentre

try(Stream<String> s=proxy(IntStream.range(0, 100).parallel().filter(i -> i/20%2==0) 
     .mapToObj(ix->"\""+ix+'"'))) { 
    s.forEach(str->{}); 
} 

stamperà

                    ["0".."99"]                      
               ___________________________________________/\___________________________________________            
              |                      |           
             ["0".."49"]                    ["50".."99"]          
         ____________________/\______________________           ______________________/\___________________      
         |           |           |           |      
        ["0".."19"]         ["40".."49"]        ["50".."59"]        ["80".."99"]     
      ____________/\_________      ____________/\______       _______/\___________     ____________/\________    
      |      |     |     |       |     |     |      |    
    ["0".."11"]    ["12".."19"]   (empty)   ["40".."49"]    ["50".."59"]   (empty)  ["80".."86"]   ["87".."99"]  
     _____/\___    _____/\_____   ___/\__   _____/\_____    _____/\_____   ___/\__   _____/\__    _____/\_____  
    |   |   |   |   |  |   |   |   |   |   |  |  |   |   |   |  
["0".."5"] ["6".."11"] ["12".."17"] ["18".."19"] (empty) (empty) ["40".."42"] ["43".."49"] ["50".."55"] ["56".."59"] (empty) (empty) ["80"] ["81".."86"] ["87".."92"] ["93".."99"] 

Come possiamo vedere qui, stiamo monitorando il risultato di .filter(…).mapToObj(…) ma i blocchi sono chiaramente determinati dalla sorgente, probabilmente producendo pezzi vuoti a valle a seconda delle condizioni del filtro.

noti che siamo in grado di combinare il monitoraggio della sorgente con il monitoraggio di Tagir collector:

try(IntStream s=proxy(IntStream.range(0, 100))) { 
    s.parallel().filter(i -> i/20%2==0) 
    .boxed().collect(parallelVisualize()) 
    .forEach(System.out::println); 
} 

questo stampa (si noti che l'uscita collect viene stampato prima):

               [0..99]                
            ________________________________/\_______________________________         
           |                 |         
          [0..49]               [50..99]        
       ________________/\______________         _______________/\_______________     
       |        |         |        |    
      [0..19]       [40..49]       [50..59]       [80..99]    
     ________/\_____     ________/\______     _______/\_______    ________/\_____   
     |    |    |    |     |    |    |    |   
    [0..11]   [12..19]   (empty)   [40..49]   [50..59]   (empty)  [80..86]  [87..99]  
    ___/\_   ___/\___   ___/\__   ___/\___   ___/\___   ___/\__  ___/\_   ___/\___  
    |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | 
[0..5] [6..11] [12..17] [18..19] (empty) (empty) [40..42] [43..49] [50..55] [56..59] (empty) (empty) [80] [81..86] [87..92] [93..99] 

                    [0..99]                 
            ___________________________________/\________________________________          
            |                  |         
           [0..49]                [50..99]         
       _________________/\______________          _________________/\________________     
       |         |         |         |     
      [0..24]       [25..49]       [50..74]       [75..99]    
     ________/\_____     ________/\_______     ________/\_______     ________/\_______   
     |    |     |     |     |     |     |     |   
    [0..11]   [12..24]   [25..36]   [37..49]   [50..61]   [62..74]   [75..86]   [87..99]  
    ___/\_   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___  
    |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | 
[0..5] [6..11] [12..17] [18..24] [25..30] [31..36] [37..42] [43..49] [50..55] [56..61] [62..67] [68..74] [75..80] [81..86] [87..92] [93..99] 

Si può vedere chiaramente come i pezzi della partita di elaborazione, ma dopo il filtraggio, alcuni blocchi hanno meno elementi, alcuni dei quali sono completamente vuoti.

Questo è il luogo di dimostrare, in cui i due modi di monitoraggio possono fare una differenza significativa:

try(DoubleStream is=proxy(DoubleStream.iterate(0, i->i+1)).parallel().limit(100)) { 
    is.boxed() 
     .collect(parallelVisualize()) 
     .forEach(System.out::println); 
} 
                       [0.0..99.0]                         
                ___________________________________________________/\________________________________________________              
                |                          |             
              [0.0..49.0]                       [50.0..99.0]            
         _________________________/\______________________              _________________________/\________________________       
         |             |             |             |       
        [0.0..24.0]          [25.0..49.0]          [50.0..74.0]          [75.0..99.0]      
      ____________/\_________       ____________/\___________       ____________/\___________       ____________/\___________    
      |      |       |       |       |       |       |       |    
    [0.0..11.0]    [12.0..24.0]    [25.0..36.0]    [37.0..49.0]    [50.0..61.0]    [62.0..74.0]    [75.0..86.0]    [87.0..99.0]  
     _____/\___    _____/\_____    _____/\_____    _____/\_____    _____/\_____    _____/\_____    _____/\_____    _____/\_____  
    |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |  
[0.0..5.0] [6.0..11.0] [12.0..17.0] [18.0..24.0] [25.0..30.0] [31.0..36.0] [37.0..42.0] [43.0..49.0] [50.0..55.0] [56.0..61.0] [62.0..67.0] [68.0..74.0] [75.0..80.0] [81.0..86.0] [87.0..92.0] [93.0..99.0] 

          [0.0..10239.0]        
     _____________________________/\_____        
     |         |        
[0.0..1023.0]      [1024.0..10239.0]      
         ____________________/\_______      
         |        |      
       [1024.0..3071.0]    [3072.0..10239.0]    
             ____________/\______    
             |     |    
           [3072.0..6143.0]  [6144.0..10239.0]  
                 ___/\_______  
                 |   | 
               [6144.0..10239.0] (empty) 

Questo dimostra what Tagir already explained, ruscelli con una dimensione sconosciuta dividere male, e anche il fatto che il limit(…) offre la possibilità di una buona stima (in realtà, il limite infinito + è teoricamente prevedibile), l'implementazione non ne trae alcun vantaggio.

La sorgente viene suddivisa in blocchi utilizzando una dimensione di batch di 1024, aumentata di 1024 dopo ogni divisione, creando blocchi fuori dall'intervallo imposto da limit. Possiamo anche vedere come viene separato un prefisso ogni volta.

Ma quando guardiamo all'uscita split del terminale, possiamo vedere che tra questi pezzi in eccesso sono stati eliminati e che è avvenuta un'altra divisione del primo blocco. Dato che questo chunk è back-end da una matrice intermedia che è stata riempita dall'implementazione predefinita sulla prima divisione, non la notiamo all'origine ma possiamo vedere all'azione del terminale che questa matrice è stata divisa (non sorprendentemente) ben bilanciata .

Quindi abbiamo bisogno di entrambi i modi di monitoraggio per ottenere l'immagine completa qui ...

+2

Davvero un'ottima aggiunta, grazie mille! –

17

L'implementazione dell'API Stream corrente utilizza collector combiner per combinare i risultati intermedi esattamente nello stesso modo in cui erano stati precedentemente suddivisi. Anche la strategia di splitting dipende dal livello di parallelismo di sorgente e di pool comune, ma non dipende dall'operazione di riduzione esatta utilizzata (lo stesso per reduce, collect, forEach, count, ecc.). Basandosi su questo non è molto difficile creare collettore visualizzazione:

public static Collector<Object, ?, List<String>> parallelVisualize() { 
    class Range { 
     private String first, last; 
     private Range left, right; 

     void accept(Object obj) { 
      if (first == null) 
       first = obj.toString(); 
      else 
       last = obj.toString(); 
     } 

     Range combine(Range that) { 
      Range p = new Range(); 
      p.first = first == null ? that.first : first; 
      p.last = Stream 
        .of(that.last, that.first, this.last, this.first) 
        .filter(Objects::nonNull).findFirst().orElse(null); 
      p.left = this; 
      p.right = that; 
      return p; 
     } 

     String pad(String s, int left, int len) { 
      if (len == s.length()) 
       return s; 
      char[] result = new char[len]; 
      Arrays.fill(result, ' '); 
      s.getChars(0, s.length(), result, left); 
      return new String(result); 
     } 

     public List<String> finish() { 
      String cur = toString(); 
      if (left == null) { 
       return Collections.singletonList(cur); 
      } 
      List<String> l = left.finish(); 
      List<String> r = right.finish(); 
      int len1 = l.get(0).length(); 
      int len2 = r.get(0).length(); 
      int totalLen = len1 + len2 + 1; 
      int leftAdd = 0; 
      if (cur.length() < totalLen) { 
       cur = pad(cur, (totalLen - cur.length())/2, totalLen); 
      } else { 
       leftAdd = (cur.length() - totalLen)/2; 
       totalLen = cur.length(); 
      } 
      List<String> result = new ArrayList<>(); 
      result.add(cur); 

      char[] dashes = new char[totalLen]; 
      Arrays.fill(dashes, ' '); 
      Arrays.fill(dashes, len1/2 + leftAdd + 1, len1 + len2/2 + 1 
        + leftAdd, '_'); 
      int mid = totalLen/2; 
      dashes[mid] = '/'; 
      dashes[mid + 1] = '\\'; 
      result.add(new String(dashes)); 

      Arrays.fill(dashes, ' '); 
      dashes[len1/2 + leftAdd] = '|'; 
      dashes[len1 + len2/2 + 1 + leftAdd] = '|'; 
      result.add(new String(dashes)); 

      int maxSize = Math.max(l.size(), r.size()); 
      for (int i = 0; i < maxSize; i++) { 
       String lstr = l.size() > i ? l.get(i) : String.format("%" 
         + len1 + "s", ""); 
       String rstr = r.size() > i ? r.get(i) : String.format("%" 
         + len2 + "s", ""); 
       result.add(pad(lstr + " " + rstr, leftAdd, totalLen)); 
      } 
      return result; 
     } 

     public String toString() { 
      if (first == null) 
       return "(empty)"; 
      else if (last == null) 
       return "[" + first + "]"; 
      return "[" + first + ".." + last + "]"; 
     } 
    } 
    return Collector.of(Range::new, Range::accept, Range::combine, 
      Range::finish); 
} 

Ecco alcuni interessanti risultati ottenuti con questo collettore per mezzo della macchina 4 conduttori (risultati saranno diversi su macchine con diverso numero di availableProcessors()).

La divisione della gamma semplice:

IntStream.range(0, 100) 
     .boxed().parallel().collect(parallelVisualize()) 
     .forEach(System.out::println); 

anche dividere a 16 compiti:

                [0..99]                 
            ___________________________________/\________________________________          
            |                  |         
           [0..49]                [50..99]         
       _________________/\______________          _________________/\________________     
       |         |         |         |     
      [0..24]       [25..49]       [50..74]       [75..99]    
     ________/\_____     ________/\_______     ________/\_______     ________/\_______   
     |    |     |     |     |     |     |     |   
    [0..11]   [12..24]   [25..36]   [37..49]   [50..61]   [62..74]   [75..86]   [87..99]  
    ___/\_   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___  
    |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | 
[0..5] [6..11] [12..17] [18..24] [25..30] [31..36] [37..42] [43..49] [50..55] [56..61] [62..67] [68..74] [75..80] [81..86] [87..92] [93..99] 

Split di due torrenti concatenazione:

IntStream 
     .concat(IntStream.range(0, 10), IntStream.range(10, 100)) 
     .boxed().parallel().collect(parallelVisualize()) 
     .forEach(System.out::println); 

Come si può vedere , prima divisione un-concatena il flussi:

                  [0..99]                   
     _______________________________________________________________________/\_____                   
     |                    |                  
    [0..9]                  [10..99]                  
    __/\__          ___________________________________/\__________________________________          
    |  |          |                  |         
[0..4] [5..9]        [10..54]                [55..99]         
           _________________/\________________          _________________/\________________     
           |         |         |         |     
          [10..31]       [32..54]       [55..76]       [77..99]    
         ________/\_______     ________/\_______     ________/\_______     ________/\_______   
         |     |     |     |     |     |     |     |   
        [10..20]   [21..31]   [32..42]   [43..54]   [55..65]   [66..76]   [77..87]   [88..99]  
        ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___   ___/\___  
        |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | 
       [10..14] [15..20] [21..25] [26..31] [32..36] [37..42] [43..48] [49..54] [55..59] [60..65] [66..70] [71..76] [77..81] [82..87] [88..93] [94..99] 

Split di due flusso di concatenazione in cui il funzionamento intermedio (scatolato()) è stata eseguita prima concatenazione:

Stream.concat(IntStream.range(0, 50).boxed().parallel(), IntStream.range(50, 100).boxed()) 
     .collect(parallelVisualize()) 
     .forEach(System.out::println); 

Se uno dei flussi in entrata non è stato trasformato in parallelo modalità prima di concatenazione, si rifiuta di dividere a tutti:

        [0..99]         
            ___/\_________________________________  
            |          | 
           [0..49]        [50..99] 
       _________________/\______________       
       |         |       
      [0..24]       [25..49]      
     ________/\_____     ________/\_______     
     |    |     |     |     
    [0..11]   [12..24]   [25..36]   [37..49]    
    ___/\_   ___/\___   ___/\___   ___/\___    
    |  |  |  |  |  |  |  |    
[0..5] [6..11] [12..17] [18..24] [25..30] [31..36] [37..42] [43..49]   

Split di f latmapping:

Stream.of(0, 50) 
     .flatMap(start -> IntStream.range(start, start+50).boxed().parallel()) 
     .parallel().collect(parallelVisualize()) 
     .forEach(System.out::println); 

Flat-mappa mai parallelizza all'interno flussi di nidificate:

[0..99]  
    ____/\__  
    |  | 
[0..49] [50..99] 

stream from unknown dimensioni iteratore di 7000 elementi (vedi this answer per il contesto):

StreamSupport 
     .stream(Spliterators.spliteratorUnknownSize(
       IntStream.range(0, 7000).iterator(), 
       Spliterator.ORDERED), true) 
     .collect(parallelVisualize()).forEach(System.out::println); 

La divisione è davvero pessima, tutti aspettano la parte più grande [3072..6143]:

     [0..6999]       
    _______________________/\___      
    |       |      
[0..1023]     [1024..6999]     
       ________________/\____     
       |      |     
      [1024..3071]   [3072..6999]   
           _________/\_____   
          |    |   
         [3072..6143]  [6144..6999]  
              ___/\____  
              |   | 
            [6144..6999] (empty) 

fonte Iterator con dimensioni note:

StreamSupport 
     .stream(Spliterators.spliterator(IntStream.range(0, 7000) 
       .iterator(), 7000, Spliterator.ORDERED), true) 
     .collect(parallelVisualize()).forEach(System.out::println); 

Fornire le dimensioni rende le cose molto meglio sbloccare l'ulteriore frazionamento:

                        [0..6999]                          
      ______________________________________________________________________________________________/\________                        
      |                          |                        
    [0..1023]                        [1024..6999]                       
    _____/\__         ____________________________________________________________________/\________________________                  
    |   |        |                        |                  
[0..511] [512..1023]     [1024..3071]                     [3072..6999]                
            ____________/\___________                 ________________/\__________________________________________________     
           |       |                |                 |     
          [1024..2047]    [2048..3071]              [3072..6143]               [6144..6999]   
          _____/\_____    _____/\_____         _________________________/\________________________          ___/\___________  
          |   |   |   |        |             |          |    | 
        [1024..1535] [1536..2047] [2048..2559] [2560..3071]     [3072..4607]          [4608..6143]       [6144..6999]  (empty) 
                         ____________/\___________       ____________/\___________      _____/\_____    
                        |       |       |       |     |   |    
                       [3072..3839]    [3840..4607]    [4608..5375]    [5376..6143]  [6144..6571] [6572..6999]   
                       _____/\_____    _____/\_____    _____/\_____    _____/\_____           
                       |   |   |   |   |   |   |   |          
                     [3072..3455] [3456..3839] [3840..4223] [4224..4607] [4608..4991] [4992..5375] [5376..5759] [5760..6143]         

Ulteriori perfezionamenti tale collettore è possibile generare immagine grafica (come svg), traccia i fili in cui viene elaborato ciascun nodo, visualizza il numero di elementi per ogni gruppo e così via. Usalo se vuoi.

+4

Bella analisi e grafica. – Kayaman

+1

È interessante notare che sembra funzionare anche con il flusso filtrato perché viene chiamato il combinatore, anche se non ci sono elementi incontrati. Ma immagino, questo comportamento non è garantito. – Holger

+1

@Holger, ovviamente questa soluzione fa molto affidamento sull'attuale implementazione. –