Skip to content

Monomorphic processing of TopN queries with simple double aggregators over historical segments (part of #3798)#4079

Merged
drcrallen merged 33 commits intoapache:masterfrom
metamx:double-agg-and-historical-topn-monomorphic-processing
May 16, 2017
Merged

Monomorphic processing of TopN queries with simple double aggregators over historical segments (part of #3798)#4079
drcrallen merged 33 commits intoapache:masterfrom
metamx:double-agg-and-historical-topn-monomorphic-processing

Conversation

@leventov
Copy link
Copy Markdown
Member

@leventov leventov commented Mar 18, 2017

  • Add a Cursor specialization: HistoricalCursor
  • Add DimensionSelector specializations: SingleValueDimensionSelector, HistoricalDimensionSelector, SingleValueHistoricalDimensionSelector
  • Add a FloatColumnSelector specialization: HistoricalFloatColumnSelector
  • Monomorphic processing of TopN queries with simple double aggregators over historical segments
  • Make Indexed and ValueMatcher interfaces to extend HotLoopCallee
  • Improvements to SpecializationService

Follow-up of #3889, part of #3798

@leventov leventov added this to the 0.10.1 milestone Mar 18, 2017
@leventov leventov changed the title Monomorphic processing of TopN queries with simple double aggregators over and historical segments (part of #3798) Monomorphic processing of TopN queries with simple double aggregators over historical segments (part of #3798) Mar 18, 2017
@egor-ryashin
Copy link
Copy Markdown
Contributor

When we reach the global limit here, we begin to spam to logs on each call

     @Override
     public void run()
     {
       try {
         T specialized;
         if (specializedClassCounter.get() > maxSpecializations) {
           // Don't specialize, just instantiate the prototype class and emit a warning.
           // The "better" approach is probably to implement some kind of cache eviction from
           // PerPrototypeClassState.specializationStates. But it might be that nobody ever hits even the current
           // maxSpecializations limit, so implementing cache eviction is an unnecessary complexity.
           specialized = perPrototypeClassState.prototypeClass.newInstance();
           LOG.warn(
               "SpecializationService couldn't make more than [%d] specializations. "
               + "Not doing specialization for runtime shape[%s] and class remapping[%s], using the prototype class[%s]",
               maxSpecializations,
               specializationId.runtimeShape,
               specializationId.classRemapping,
               perPrototypeClassState.prototypeClass

@leventov
Copy link
Copy Markdown
Member Author

@egor-ryashin thanks, fixed to emit only once.

@Override
public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
inspector.visit("theBuffer", theBuffer);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok that theBuffer is not final and changed afterwards?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed theBuffer to be final

@Override
public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
inspector.visit("baseList", baseList);
Copy link
Copy Markdown
Contributor

@egor-ryashin egor-ryashin Mar 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the clazz field?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not used in get() (the only @CalledFromHotLoop method in Indexed), so it shouldn't be visited in inspectRuntimeShape()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would create an annotation like:

@Inspect
private final List<T> baseList;

make an abstract super class with a method

inspectRuntimeShape(RuntimeShapeInspector inspector) {
 
  for(Field field : this.getClass().getDeclaredFields()){
      field.isAnnotationPresent(Inspect.class);
      field.setAccessible(true);
      inspector.visit(field.getName(), field.get(this))
   }
}

and forget about overriding methods.

It's up to you though.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make it too easy to forget to annotate some fields. For the same reason inspectRuntimeShape() method doesn't have default Java 8 implementation with empty body, although it is always correct and a lot of implementations just do that: to force developers to implement and think about the runtime shape.

It's just my opinion, though. Maybe it makes sense to add annotation-based runtime shape inspection in some form. But anyway, not as part of this PR.

public abstract void aggregate(ByteBuffer buf, int position, double value);

@Override
public final void aggregate(ByteBuffer buf, int position)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method be annotated with @CalledFromHotLoop?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, because this method is annotated in BufferAggregator

Historical1AggPooledTopNScanner defaultHistoricalSingleValueDimSelector1SimpleDoubleAggScanner =
new HistoricalSingleValueDimSelector1SimpleDoubleAggPooledTopNScannerPrototype();

private final Capabilities capabilities;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need additional ref to Capabilities here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used to create BaseArrayProvider in makeInitParams().

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capabilities is protected field in BaseTopNAlgorithm

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, removed

SpecializationService.getSpecializationState(
prototypeScanner.getClass(),
runtimeShape,
ImmutableMap.<Class<?>, Class<?>>of(Offset.class, historicalCursor.getOffset().getClass())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Class<?>, Class<?>> could be removed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

)
{
return getSpecializationState(prototypeClass, runtimeShape, ImmutableMap.<Class<?>, Class<?>>of());
return getSpecializationState(prototypeClass, runtimeShape, ImmutableMap.of());
Copy link
Copy Markdown
Contributor

@dgolitsyn dgolitsyn Mar 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is synchronization required on call to perPrototypeClassState.get() from multiple threads? or call to computeValue()?
Could you please clarify, why WindowedLoopIterationCounter#getSpecialized() always returns null?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is synchronization required on call to perPrototypeClassState.get() from multiple threads? or call to computeValue()?

According to ClassValue documentation, it should be safe: "The actual installation of the value on the class is performed atomically. At that point, if several racing threads have computed values, one is chosen, and returned to all the racing threads."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please clarify, why WindowedLoopIterationCounter#getSpecialized() always returns null?

Added comment

@Override
public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
inspector.visit("delegate", delegate);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add lookupCacheSize to runtimeShape?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or lookupCacheSize != 0

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added, thanks

@leventov
Copy link
Copy Markdown
Member Author

@fjy we run this in production since December: charts in #3798 show that.

}

@Override
public void inspectRuntimeShape(RuntimeShapeInspector inspector)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

baseArray is not inspected, but is used in get()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used just for indexing, there should be no difference for Hotspot for any Object array type. So no need to distinguish between String[] and Object[], for example.

@Override
public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
inspector.visit("cachedValues", cachedValues != null);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why we does not care about cachedValues element type?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because CachingIndexed.get() doesn't call methods on instances of this type directly. So for Hotspot, it's just an object, doesn't matter of what type. If it does matter in delegate.get(), delegate's runtime shape should reflect this, delegate is inspected in CachingIndexed.inspectRuntimeShape().

@Override
public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
// ideally should inspect buffer, but at the moment of inspectRuntimeShape() call buffer might be null although
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we get those fields with subclasses different from the original object then we still get the first mapping and ignore the second implementation and eventually get unexpected/different results on dash, right?

Copy link
Copy Markdown
Member Author

@leventov leventov Mar 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't understand this question. However any implementation of inspectRuntimeShape() anywhere doesn't affect correctness.

The purpose of inspectRuntimeShape() is to ensure that some copy of the code is always called with the same runtime shape (monomorphic), and (ideally) there is only one copy of the code for each runtime shape, not to make JIT perform the same compilation work twice and waste code cache (on Hotspot level) and instruction cache (on hardware level).

Not reflecting all fields which actually belong to runtime shape in inspectRuntimeShape() could lead to the situation when some copy of the code is called with non-monomorphic runtime shape, that will make Hotspot JIT to generate slower (but still correct) code.

Reflecting some fields which actually don't belong to runtime shape in inspectRuntimeShape() could lead to the situation when there are several copies of the code which are called with the same runtime shape, that will pollute code cache and will make JIT to perform the same work several times, but the code is still correct.

@Override
public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
inspector.visit("buffer", buffer);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I see this is a mutable field. Could have "null", then something else, but description will be 'null' for all the instances, right?
Also 'bigEndian' is changed during processing when loadBuffer() is called.

Copy link
Copy Markdown
Member Author

@leventov leventov Mar 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's bad that buffer couldn't be properly inspected here. Probably this situation could be improved later.

Also 'bigEndian' is changed during processing when loadBuffer() is called.

Thanks, removed inspection of bigEndian too.

}

return new DimensionSelector()
return new SingleValueDimensionSelector()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an inspection of extractionFn below, but I don't see it plays as a 'boolean flag'.
io/druid/query/groupby/RowBasedColumnSelectorFactory.java:180

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used in lookupName() which is @CalledFromHotLoop

Offset offset = (Offset) TopNUtils.copyOffset(cursor);
long scannedRows = 0;
int positionToAllocate = 0;
while (offset.withinBounds() && !Thread.currentThread().isInterrupted()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is interrupted, where is it intended to be caught?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In PooledTopNAlgorithm.scanAndAggregate(), BaseQuery.checkInterrupted() is called in the end of each branch.

Offset offset = (Offset) TopNUtils.copyOffset(cursor);
long scannedRows = 0;
int positionToAllocate = 0;
while (offset.withinBounds() && !Thread.currentThread().isInterrupted()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like we should be able to use an Iterator or IntStream here, but I'm not finding anything reasonable in the standard java libraries at a cursory glance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is IntIterator from fastutil, and even https://docs.oracle.com/javase/8/docs/api/java/util/PrimitiveIterator.OfInt.html.

I don't think such change should be part of this PR.

if (specializeGeneric1AggPooledTopN && theAggregators.length == 1) {
scanAndAggregateGeneric1Agg(params, positions, theAggregators[0], cursor);
} else if (specializeGeneric2AggPooledTopN && theAggregators.length == 2) {
if (theAggregators.length == 1) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we abstract the predicate / function checking into a List of Pairs of predicates and runnables or similar? and loop through the list until it finds a matching predicate to run? Then to deny specializeHistoricalSingleValueDimSelector1SimpleDoubleAggPooledTopN is simply to not add it in the list.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those BaseQuery.checkInterrupted(); + return; are pretty buried and would be easy to miss in future additions. So having a loop and finishing on those two items feels more sustainable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice idea, implemented

)
{
String runtimeShape = StringRuntimeShape.of(aggregator);
HistoricalCursor historicalCursor = (HistoricalCursor) cursor;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest only taking in HistoricalCursor and doing the cast in the caller

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed


@Override
public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If really nothing suggest also adding comment as per the other examples below

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
inspector.visit("firstBaseMatcher", baseMatchers[0]);
inspector.visit("secondBaseMatcher", baseMatchers[1]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What guarantees baseMatchers.length() > 1?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 109-113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I see, >0 and !=1 ok

public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
inspector.visit("baseSelector", baseSelector);
inspector.visit("extractionFn", extractionFn);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this a bug?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Copy Markdown
Contributor

@drcrallen drcrallen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of neat added functionality that seems to be ok, but there is a severe lack of unit tests proving to future developers that they don't mess it up somehow. Please add unit tests for the new features.

Also, there seems to be a lot of discussion or changes around what needs to be included in inspectRuntimeShape. Can checks for that be automated or at least tested in unit tests somehow?

@leventov leventov added the WIP label May 11, 2017
…type and HistoricalSingleValueDimSelector1SimpleDoubleAggPooledTopNScannerPrototype, cover them with tests
@leventov leventov added Performance and removed WIP labels May 11, 2017
@leventov
Copy link
Copy Markdown
Member Author

@drcrallen I've changed existing tests so that they cover all added "prototypes" (and it helped to find bugs).

Regarding testing inspectRuntimeShape(), I don't think it's needed. Mistakes in inspectRuntimeShape would cause performance regression at worst, and to the level not worse than pre-#3798.

Manual testing of inspectRuntimeShape() is

  1. a giant amount of work
  2. often not possible, with anonymous or private classes
  3. still won't help much, because most likely type of mistake is adding a new field to HotLoopCallee-extending class, that should be inspected, but forgetting to add it to inspectRuntimeShape(). But in this case manual test would still pass, because it is also not updated.

Ways to test inspectRuntimeShape() automatically could exist, some approaches possibly similar to mocking, when entering @CalledFromHotLoop-annotated methods is recorded, entering downstream methods is recorded, and checking that for the same runtime shape, the same methods are called at all downstream call sites. However it's a huge and complex task to develop such automated inspectRuntimeShape() testing framework, and definitely not as part of this PR.

@Override
public Offset clone()
{
FilteredOffset offset = (FilteredOffset) super.clone();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What guarantees an Offset clone is castable like this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It delegates to Object.clone(), which returns instance of the same class as this.

import io.druid.segment.historical.OffsetHolder;
import org.roaringbitmap.IntIterator;

final class FilteredOffset extends Offset
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class seems to have no unit tests

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class if factored out of QueryableIndexStorageAdapter. It didn't exist before this PR, because QueryableIndexStorageAdapter used to handle post filters using a special cursor, and now there is the same cursor accepting any provided Offset, post-filtered like FilteredOffset, or not. So this class is tested by all the same tests which test QueryableIndexStorageAdapter directly or indirectly, mostly tests of specific query types.

advance();
count++;
if (!Thread.currentThread().isInterrupted()) {
cursorOffset.increment();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incrementInterruptibly?

public int lookupId(ActualType name);
public int getCardinality();

HistoricalDimensionSelector makeDimensionSelector(OffsetHolder offsetHolder, ExtractionFn extractionFn);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if this will screw up @cheddar / @himanshug 's lucine index stuff

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to architect this so there isn't a historical-specific method in DictionaryEncodedColumn?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to return just DimensionSelector

return cachedLookups.size();
}

@Override
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to have no unit tests

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same as for FilteredOffset, this method is factored out of QueryableIndexStorageAdapter and tested through it

return true;
}
};
return makeMatcher(matchers.toArray(new ValueMatcher[0]));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new ValueMatcher[0] can be a static constant

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed


private ValueMatcher makeMatcher(final ValueMatcher[] baseMatchers)
{
Preconditions.checkState(baseMatchers.length > 0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this support a 0 length array and just return BooleanValueMatcher.of(false);?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prohibit 0 length in constructor, so here it shouldn't appear too

public ValueMatcher makeMatcher(ColumnSelectorFactory factory)
{
if (filters.size() == 0) {
return BooleanValueMatcher.of(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is odd because on one hand you end up with a value matcher that returned false here, but makeMatcher has a fall-through case of true.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makeMatcher(ValueMatcher[]) has Preconditions.checkState(baseMatchers.length > 0);, so it should end up throwing an exception anyway

public void inspectRuntimeShape(RuntimeShapeInspector inspector)
{
inspector.visit("firstBaseMatcher", baseMatchers[0]);
inspector.visit("secondBaseMatcher", baseMatchers[1]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I see, >0 and !=1 ok

/**
* Public bridge is needed, because MagicAccessorImpl is a package-private class.
*/
public class MagicAccessorBridge extends MagicAccessorImpl
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't extending sun.* packages violate the java license agreement?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but org.codehaus.groovy.reflection is not in sun.reflect

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It creates "sun/reflect/GroovyMagic" class. Actually an interesting question, asked here: https://twitter.com/leventov/status/863155057113669632

@leventov
Copy link
Copy Markdown
Member Author

On #4079 (comment): no, because it's in the method called "advanceUninterruptibly"

Copy link
Copy Markdown
Contributor

@drcrallen drcrallen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @leventov !

@drcrallen drcrallen merged commit d400f23 into apache:master May 16, 2017
@drcrallen drcrallen deleted the double-agg-and-historical-topn-monomorphic-processing branch May 16, 2017 23:19
@leventov leventov mentioned this pull request Jul 22, 2018
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants