Search code examples
javatime-complexitybig-oconcurrenthashmap

Java code execution to get result in O(1)


I have a webservice from which I gets a time and price. I have saved those records in a ConcurrentHashMap since it needs to support in a multi-threaded environment with timestamp (LocalDateTime) as key and price (BigDecimal) as the values. The requirement was to get the following details

  1. Total records in last 90 records
  2. Average records in last 90 records
  3. Lowest Price in last 90 records
  4. Highest Price in last 90 records
  5. Total Price in last 90 records
  6. Average Price in last 90 records

I have successfully achieved the requirement by the code a shown below

ConcurrentHashMap<LocalDateTime, BigDecimal> data = // my full records

int totalRecords = 0;
BigDecimal highestPrice = new BigDecimal(0.0);
BigDecimal lowestPrice = new BigDecimal(0.0);
BigDecimal totalPriceSum = new BigDecimal(0.0);
Instant currentTime = Instant.now();
Duration limit = Duration.ofSeconds(90);
for (LocalDateTime time : data.keySet()) {
    Duration duration = Duration.between(currentTime , time);
    Boolean matches = ( duration.compareTo(limit) < 0 );
    if(matches) 
    {
        BigDecimal recordPrice = data.get(time);
        if(recordPrice.compareTo(lowestPrice) < 0) {
            lowestPrice = recordPrice;
        }

        if(recordPrice.compareTo(lowestPrice) > 0) {
            highestPrice = recordPrice;
        }
        totalPriceSum = totalPriceSum.add(recordPrice);
        totalRecords++;
    }
}


System.out.println("Total records in last 90 records: "+ totalRecords);
System.out.println("Average records in last 90 records: "+ (totalRecords/90)*100);
System.out.println("Lowest Price in last 90 records: "+ lowestPrice);
System.out.println("Highest Price in last 90 records: "+ highestPrice);
System.out.println("Total Price in last 90 records: "+ totalPriceSum);
System.out.println("Average Price in last 90 records: "+ (totalPriceSum.doubleValue()/90)*100);

But my customer says this has some performance issues, and the code should run and give in O(1)

Can anyone please help me or suggest me a different approach to achieve this. Should I not use Collections in order to achieve O(1)


Solution

  • From the comments - here's an example of what I meant about calculating the exact keys to use. It still uses a LocalDateTime (instead of a Long for the nanos) as the key, but it is truncated to seconds. So there are at most 90 keys which need to be collected.

    There is aggregate PriceRequest class to hold concurrent requests within the same second. (It's not completely thread safe.)

    public class Last90Seconds {
        private Map<LocalDateTime, PriceRequest> priceRequests = new ConcurrentHashMap<>();
    
        public static void main(String[] args) throws Exception {
            Last90Seconds app = new Last90Seconds();
            app.simulatePriceRequests();  // thread which continuously simulates a price request
    
            for (int i = 0; i < 10; i++) {
                Thread.sleep(9000);
                app.reportOnPriceRequests();
            }
        }
    
        private void simulatePriceRequests() {
            new Thread(new RequestForPriceSimulator()).start();
        }
    
        private void reportOnPriceRequests() {
            long startNanos = System.nanoTime();
            new ReportSimulator().generateReport();
            long elapsedNanos = System.nanoTime() - startNanos;
            System.out.println("Took " + elapsedNanos / 1000.0 + " milliseconds to generate report.\n\n");
        }
    
        private LocalDateTime truncateToSeconds(LocalDateTime ldt) {
            return ldt.truncatedTo(ChronoUnit.SECONDS);
        }
    
        private PriceRequest getPriceTracker(LocalDateTime key) {
            return priceRequests.get(key);
        }
    
        private PriceRequest getPriceTrackerEvenIfAbsent(LocalDateTime key) {
            return priceRequests.computeIfAbsent(key, v -> new PriceRequest());
        }
    
        public class RequestForPriceSimulator implements Runnable {
    
            @Override
            public void run() {
                LocalDateTime rightNow = truncateToSeconds(LocalDateTime.now());
                LocalDateTime ninentySecondsFromNow = rightNow.plusSeconds(90);
                while (rightNow.isBefore(ninentySecondsFromNow)) {
    
                    PriceRequest pt = getPriceTrackerEvenIfAbsent(rightNow);
                    double price = ThreadLocalRandom.current().nextDouble() * 10.0;
                    pt.addRequest(price);
    
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    rightNow = truncateToSeconds(LocalDateTime.now());
                }
    
                System.out.println("All done simulating a price request!\n");
            }
        }
    
        public class ReportSimulator {
    
            public void generateReport() {
                double lowest = Double.MAX_VALUE;
                double highest = Double.MIN_VALUE;
                double total = 0;
                long requestCounter = 0;
    
                int keyCounter = 0;
                int validKeyCounter = 0;
    
                LocalDateTime rightNow = truncateToSeconds(LocalDateTime.now());
                LocalDateTime key = rightNow.minusSeconds(90);
                while (key.isBefore(rightNow)) {
                    keyCounter++;
    
                    key = key.plusSeconds(1);
    
                    PriceRequest pt = getPriceTracker(key);
                    if (pt == null) {
                        continue;
                    }
    
                    validKeyCounter++;
                    if (pt.getLowest() < lowest) {
                        lowest = pt.getLowest();
                    }
    
                    if (pt.getHighest() < highest) {
                        highest = pt.getHighest();
                    }
    
                    total += pt.getTotal();
                    requestCounter += pt.getCounter();
                }
    
                System.out.println("Used " + validKeyCounter + " keys out of " + keyCounter + " possible keys.");
                System.out.println("Total records in last 90 seconds: " + requestCounter);
                System.out.println("Average records per second in last 90 seconds: " + requestCounter / 90);
                System.out.println("Lowest Price in last 90 seconds: " + lowest);
                System.out.println("Highest Price in last 90 seconds: " + highest);
                System.out.println("Total Price in last 90 seconds: " + total);
                System.out.println("Average Price in last 90 seconds: " + (total / requestCounter));
            }
        }
    
        public class PriceRequest {
            private long counter;
            private double lowest;
            private double highest;
            private double total;
    
            public PriceRequest() {
                lowest = Double.MAX_VALUE;
                highest = Double.MIN_VALUE;
            }
    
            public void addRequest(double price) {
                synchronized (this) {
    
                    if (price < lowest) {
                        lowest = price;
                    }
    
                    if (price > highest) {
                        highest = price;
                    }
    
                    total += price;
                    counter++;
                }
            }
    
            public double getCounter() {
                synchronized (this) {
                    return counter;
                }
            }
    
            public double getLowest() {
                synchronized (this) {
                    return lowest;
                }
            }
    
            public double getHighest() {
                synchronized (this) {
                    return highest;
                }
            }
    
            public double getTotal() {
                synchronized (this) {
                    return total;
                }
            }
        }
    
    }