Access your data

We analyze the full taxi data as described by Todd Schneider in using R and sparklyr. We load the billion record trips table into Apache Spark and then use sparklyr and dplyr to manipulate the data and run machine learning algorithms at scale.

The data represent 200 GB of uncompressed data in CSV format. When converted and compressed in the parquet format, the data are 70 GB. The data are stored in HDFS and pre-loaded in a Hive table.

The Hadoop cluster runs on Elastic Map Reduce (EMR) in AWS and has 12 worker nodes and one master node. The master node has R, RStudio Server Pro, and sparklyr loaded onto it.

Connect to spark

Use sparklyr to create a new connection to Apache Spark.

# Load libraries
library(ggplot2)
library(leaflet)
library(geosphere)
library(tidyr)
library(shiny)
library(sparklyr)
library(dplyr)
library(miniUI)
library(DT)
# Configure cluster
Sys.setenv(SPARK_HOME="/usr/lib/spark")
config <- spark_config()
config$spark.driver.cores   <- 32
config$spark.executor.cores <- 32
config$spark.executor.memory <- "40g"
# Connect to cluster
sc <- spark_connect(master = "yarn-client", config = config, version = '1.6.1')

Prepare your data

# Create table references
trips_tbl <- tbl(sc, "trips_par")
nyct2010_tbl <- tbl(sc, "nyct2010")
# Join tables
trips_joined_tbl <- trips_tbl %>%
  filter(!is.na(pickup_nyct2010_gid) & !is.na(dropoff_nyct2010_gid)) %>%
  select(pickup_datetime, dropoff_datetime,
         pickup_latitude, dropoff_latitude, 
         pickup_longitude, dropoff_longitude,
         pickup_nyct2010_gid, dropoff_nyct2010_gid,
         tip_amount, fare_amount, vendor_id, passenger_count, trip_distance) %>%
  left_join(
    select(nyct2010_tbl, pickup_gid = gid, pickup_boro = boroname, pickup_nta = ntaname), 
    by = c("pickup_nyct2010_gid" = "pickup_gid")) %>%
  left_join(
    select(nyct2010_tbl, dropoff_gid = gid, dropoff_boro = boroname, dropoff_nta = ntaname), 
    by = c("dropoff_nyct2010_gid" = "dropoff_gid")) %>%
  sdf_register("trips_par_joined")
# Cache table
tbl_cache(sc, "trips_par_joined")

Understand your data

Spark SQL and dplyr

Use dplyr syntax to write Spark SQL. When you’re ready to visualize your data, use collect to bring data into R memory. Notice the data describe roughly 170 million trips per year.

# Calculate total trips
trips_joined_tbl %>% count
Source:   query [?? x 1]
Database: spark connection master=yarn-client app=sparklyr local=FALSE

           n
       <dbl>
1 1184591756
# Calculate trips by year
trip_by_year <- trips_joined_tbl %>%
  mutate(year = year(pickup_datetime)) %>%
  group_by(year) %>%
  summarize(n = n()) %>%
  collect()
# Plot trips by year
ggplot(trip_by_year, aes(year, n)) + 
  geom_bar(stat="Identity") +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Number of trips by year", x = "Year", y = "")

You can measure the time between pickup and dropoff for any two locations. In this example, we measure the time between JFK and mid-town. Notice the longest trip times occur around 4 PM.

# Calcluate trip time for a specific pickup and dropoff
pickup_dropoff_tbl <- trips_joined_tbl %>%
  filter(pickup_nyct2010_gid == 1250 & dropoff_nyct2010_gid == 2056) %>%
  mutate(pickup_hour = hour(pickup_datetime)) %>%
  mutate(trip_time = unix_timestamp(dropoff_datetime) - unix_timestamp(pickup_datetime)) %>%
  group_by(pickup_hour) %>% 
  summarize(n = n(),
            trip_time_mean = mean(trip_time),
            trip_time_p10 = percentile(trip_time, 0.10),
            trip_time_p25 = percentile(trip_time, 0.25),
            trip_time_p50 = percentile(trip_time, 0.50),
            trip_time_p75 = percentile(trip_time, 0.75),
            trip_time_p90 = percentile(trip_time, 0.90))
# Collect results
pickup_dropoff <- collect(pickup_dropoff_tbl)
# Plot
ggplot(pickup_dropoff, aes(x = pickup_hour)) +
          geom_line(aes(y = trip_time_p50, alpha = "Median")) +
          geom_ribbon(aes(ymin = trip_time_p25, ymax = trip_time_p75, 
                          alpha = "25–75th percentile")) +
          geom_ribbon(aes(ymin = trip_time_p10, ymax = trip_time_p90, 
                          alpha = "10–90th percentile")) +
          scale_y_continuous("trip duration in minutes")

Spark ML

With Spark ML you can run machine learning algorithms against all your data in Spark. Here we attempt to understand what factors influence tip amounts.

# Select a model data set
model_tbl <- trips_joined_tbl %>%
  filter(pickup_nyct2010_gid == 1250 & dropoff_nyct2010_gid == 2056) %>%
  mutate(pickup_hour = hour(pickup_datetime)) %>%
  mutate(pickup_week = weekofyear(pickup_datetime)) %>%
  mutate(pickup_year = year(pickup_datetime)) %>%
  mutate(trip_time = unix_timestamp(dropoff_datetime) - unix_timestamp(pickup_datetime)) %>%
  filter(!is.na(pickup_nyct2010_gid) & !is.na(dropoff_nyct2010_gid)) %>%
  filter(!is.na(tip_amount)) %>%
  filter(!is.na(fare_amount)) %>%
  filter(!is.na(vendor_id)) %>%
  filter(!is.na(pickup_hour)) %>%
  filter(!is.na(pickup_week)) %>%
  filter(!is.na(passenger_count)) %>%
  filter(!is.na(trip_time)) %>%
  filter(!is.na(trip_distance))
# Partitioin into train and validate
model_partition_tbl <- model_tbl %>%
  sdf_partition(train = 0.8, test = 0.2, seed = 1234)
# Create table references
trips_train_tbl <- sdf_register(model_partition_tbl$train, "trips_train")
trips_test_tbl <- sdf_register(model_partition_tbl$train, "trips_test")
# Cache model data
tbl_cache(sc, "trips_train")
# Model data
model_formula <- formula(tip_amount ~ 
                           fare_amount + vendor_id + pickup_hour + pickup_week + 
                           passenger_count + trip_time + trip_distance)
m1 <- ml_linear_regression(trips_train_tbl, model_formula)
summary(m1)
Call: ml_linear_regression(trips_train_tbl, model_formula)

Deviance Residuals: (approximate):
     Min       1Q   Median       3Q      Max 
-25.2997  -4.6801  -0.9295   5.0750 144.4012 

Coefficients:
                   Estimate  Std. Error  t value  Pr(>|t|)    
(Intercept)     -1.3565e+00  1.6172e-01  -8.3879 < 2.2e-16 ***
fare_amount      1.7752e-01  2.7468e-03  64.6287 < 2.2e-16 ***
vendor_id_2      4.6725e-01  7.6849e-02   6.0800 1.205e-09 ***
vendor_id_CMT   -1.5830e+00  6.2922e-02 -25.1581 < 2.2e-16 ***
vendor_id_DDS   -3.1478e+00  1.3872e-01 -22.6917 < 2.2e-16 ***
vendor_id_VTS   -1.4759e+00  6.2890e-02 -23.4686 < 2.2e-16 ***
pickup_hour     -2.1475e-02  3.5354e-03  -6.0743 1.248e-09 ***
pickup_week      1.9910e-03  9.9476e-04   2.0015   0.04535 *  
passenger_count -2.5653e-01  1.1200e-02 -22.9058 < 2.2e-16 ***
trip_time       -8.0990e-06  8.6924e-06  -0.9317   0.35148    
trip_distance   -9.4866e-05  7.1767e-05  -1.3219   0.18622    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

R-Squared: 0.05971
Root Mean Squared Error: 5.526

HTML Widgets

Spark makes it easy to drill down to any level of you data. You can visualize the top dropoffs for any pickup location by entering your pickup location. In this example the pickup location is JFK International Airport.

# Summarize trips from JFK Airport
jfk_pickup_tbl <- trips_joined_tbl %>%
  filter(pickup_nyct2010_gid == 2056) %>%
  filter(!is.na(dropoff_nyct2010_gid)) %>%
  mutate(trip_time = unix_timestamp(dropoff_datetime) - unix_timestamp(pickup_datetime)) %>%
  group_by(dropoff_nyct2010_gid) %>% 
  summarize(n = n(),
            trip_time_mean = mean(trip_time),
            trip_dist_mean = mean(trip_distance),
            dropoff_latitude = mean(dropoff_latitude),
            dropoff_longitude = mean(dropoff_longitude),
            passenger_mean = mean(passenger_count),
            fare_amount = mean(fare_amount),
            tip_amount = mean(tip_amount))
# Collect top results
jfk_pickup <- jfk_pickup_tbl %>%
  mutate(n_rank = min_rank(desc(n))) %>%
  filter(n_rank <= 25) %>%
  collect
# Plot top trips on map
leaflet(jfk_pickup) %>% 
  setView(lng = -73.9, lat = 40.7, zoom = 11) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addCircleMarkers(~dropoff_longitude, ~dropoff_latitude, stroke = F, color = "red") %>%
  addCircleMarkers(-73.7781, 40.6413, fill = FALSE, color = "green")
Note: the specification for S3 class “AsIs” in package ‘jsonlite’ seems equivalent to one from package ‘DBI’: not turning on duplicate class definitions for this class.

Shiny Gadget

It is often useful to visualize the analyses interactively. Use a Shiny gadget to explore other pickup and dropoff locations.

# Create list of geo groups to select from
geo_group <- trips_joined_tbl %>% 
  distinct(pickup_nta) %>%
  arrange(pickup_nta) %>%
  collect
# Create the gadget user interface
ui <- miniPage(
  gadgetTitleBar("NYC Taxi Trips"),
  miniTabstripPanel(
    miniTabPanel("Inputs", icon = icon("sliders"),
                 miniContentPanel(
                   selectInput("pickup",  "Taxi Pickup", geo_group, "Lincoln Square"),
                   selectInput("dropoff",  "Taxi Dropoff", geo_group, "Upper West Side")
                   )
                 ),
    miniTabPanel("Plot", icon = icon("area-chart"),
                 miniContentPanel(
                   plotOutput("tripTimePlot")
                   )
    ),
    miniTabPanel("Map", icon = icon("map-o"),
                 miniContentPanel(
                   leafletOutput("tripLeaflet")
                 )
    ),
    miniTabPanel("Data", icon = icon("table"),
                 miniContentPanel(
                   dataTableOutput("table", height = "100%")
                 )
    )
  )
)
# Create the shiny gadget functions
server <- function(input, output) {
  shiny_pickup_dropoff_hour <- reactive({
    trips_joined_tbl %>%
    filter(pickup_nta == input$pickup & dropoff_nta == input$dropoff) %>%
    mutate(pickup_hour = hour(pickup_datetime)) %>%
    mutate(trip_time = unix_timestamp(dropoff_datetime) - unix_timestamp(pickup_datetime)) %>%
    group_by(pickup_hour) %>% 
    summarize(n = n(),
              pickup_latitude = mean(pickup_latitude),
              pickup_longitude = mean(pickup_longitude),
              dropoff_latitude = mean(dropoff_latitude),
              dropoff_longitude = mean(dropoff_longitude),
              trip_time_mean = mean(trip_time),
              trip_time_p10 = percentile(trip_time, 0.10),
              trip_time_p25 = percentile(trip_time, 0.25),
              trip_time_p50 = percentile(trip_time, 0.50),
              trip_time_p75 = percentile(trip_time, 0.75),
              trip_time_p90 = percentile(trip_time, 0.90)) %>%
    collect
  })
  shiny_pickup_dropoff <- reactive({
    shiny_pickup_dropoff_hour() %>%
      summarize(n = n(),
                pickup_latitude = mean(pickup_latitude),
                pickup_longitude = mean(pickup_longitude),
                dropoff_latitude = mean(dropoff_latitude),
                dropoff_longitude = mean(dropoff_longitude))
  })
  
  output$tripTimePlot <- renderPlot({
    ggplot(shiny_pickup_dropoff_hour(), aes(x = pickup_hour)) +
    geom_line(aes(y = trip_time_p50 / 60, alpha = "Median")) +
    geom_ribbon(aes(ymin = trip_time_p25 / 60, 
                    ymax = trip_time_p75 / 60, 
                    alpha = "25–75th percentile")) +
    geom_ribbon(aes(ymin = trip_time_p10 / 60, 
                    ymax = trip_time_p90 / 60, 
                    alpha = "10–90th percentile")) +
    scale_y_continuous("trip duration in minutes") + 
    ggtitle("Trip time in minutes")
   })
  output$tripLeaflet <- renderLeaflet({  
      leaflet(shiny_pickup_dropoff()) %>% 
      addProviderTiles("CartoDB.Positron") %>%
      addCircleMarkers(~pickup_longitude, ~pickup_latitude, fill = FALSE, color = "green") %>%
      addCircleMarkers(~dropoff_longitude, ~dropoff_latitude, stroke = FALSE, color = "red")
  })
  
  output$table <- renderDataTable({
    shiny_pickup_dropoff_hour() %>%
      mutate(trip_time_mean = round(trip_time_mean / 60)) %>%
      mutate(trip_time_p50 = round(trip_time_p50 / 60)) %>%
      select(pickup_hour, n, trip_time_mean, trip_time_p50)
  })
  
  observeEvent(input$done, {
    stopApp(TRUE)
  })
}
# Run the gadget
runGadget(ui, server)
[1] TRUE

Communicate your insights

We estimated taxi trip times by time of day for multiple pickup and dropoff locations. We found the key drivers for predicting tip amounts for specific pickup and dropoff locations. We also found the top dropoff locations for specific pickup locations. Finally, we published these results in an R markdown notebook.

LS0tCnRpdGxlOiAiQW5hbHl6aW5nIGEgYmlsbGlvbiBOWUMgdGF4aSB0cmlwcyBpbiBTcGFyayIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKYGBge3Igc2V0dXAsIGV2YWw9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIGluY2x1ZGU9RkFMU0V9Cmluc3RhbGwucGFja2FnZXMoJ21pbmlVSScpCmluc3RhbGwucGFja2FnZXMoJ0RUJykKYGBgCgojIEFjY2VzcyB5b3VyIGRhdGEKCldlIGFuYWx5emUgdGhlIGZ1bGwgdGF4aSBkYXRhIGFzIGRlc2NyaWJlZCBieSBbVG9kZCBTY2huZWlkZXJdKGh0dHA6Ly90b2Rkd3NjaG5laWRlci5jb20vcG9zdHMvYW5hbHl6aW5nLTEtMS1iaWxsaW9uLW55Yy10YXhpLWFuZC11YmVyLXRyaXBzLXdpdGgtYS12ZW5nZWFuY2UvKSBpbiB1c2luZyBSIGFuZCBzcGFya2x5ci4gV2UgbG9hZCB0aGUgYmlsbGlvbiByZWNvcmQgdHJpcHMgdGFibGUgaW50byBBcGFjaGUgU3BhcmsgYW5kIHRoZW4gdXNlIHNwYXJrbHlyIGFuZCBkcGx5ciB0byBtYW5pcHVsYXRlIHRoZSBkYXRhIGFuZCBydW4gbWFjaGluZSBsZWFybmluZyBhbGdvcml0aG1zIGF0IHNjYWxlLgoKVGhlIGRhdGEgcmVwcmVzZW50IDIwMCBHQiBvZiB1bmNvbXByZXNzZWQgZGF0YSBpbiBDU1YgZm9ybWF0LiBXaGVuIGNvbnZlcnRlZCBhbmQgY29tcHJlc3NlZCBpbiB0aGUgcGFycXVldCBmb3JtYXQsIHRoZSBkYXRhIGFyZSA3MCBHQi4gVGhlIGRhdGEgYXJlIHN0b3JlZCBpbiBIREZTIGFuZCBwcmUtbG9hZGVkIGluIGEgSGl2ZSB0YWJsZS4KClRoZSBIYWRvb3AgY2x1c3RlciBydW5zIG9uIEVsYXN0aWMgTWFwIFJlZHVjZSAoRU1SKSBpbiBBV1MgYW5kIGhhcyAxMiB3b3JrZXIgbm9kZXMgYW5kIG9uZSBtYXN0ZXIgbm9kZS4gVGhlIG1hc3RlciBub2RlIGhhcyBSLCBSU3R1ZGlvIFNlcnZlciBQcm8sIGFuZCBzcGFya2x5ciBsb2FkZWQgb250byBpdC4KCiMjIENvbm5lY3QgdG8gc3BhcmsKClVzZSBzcGFya2x5ciB0byBjcmVhdGUgYSBuZXcgY29ubmVjdGlvbiB0byBBcGFjaGUgU3BhcmsuIAoKYGBge3IgY29ubmVjdCwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KIyBMb2FkIGxpYnJhcmllcwpsaWJyYXJ5KGdncGxvdDIpCmxpYnJhcnkobGVhZmxldCkKbGlicmFyeShnZW9zcGhlcmUpCmxpYnJhcnkodGlkeXIpCmxpYnJhcnkoc2hpbnkpCmxpYnJhcnkoc3BhcmtseXIpCmxpYnJhcnkoZHBseXIpCmxpYnJhcnkobWluaVVJKQpsaWJyYXJ5KERUKQoKIyBDb25maWd1cmUgY2x1c3RlcgpTeXMuc2V0ZW52KFNQQVJLX0hPTUU9Ii91c3IvbGliL3NwYXJrIikKY29uZmlnIDwtIHNwYXJrX2NvbmZpZygpCmNvbmZpZyRzcGFyay5kcml2ZXIuY29yZXMJPC0gMzIKY29uZmlnJHNwYXJrLmV4ZWN1dG9yLmNvcmVzCTwtIDMyCmNvbmZpZyRzcGFyay5leGVjdXRvci5tZW1vcnkgPC0gIjQwZyIKCiMgQ29ubmVjdCB0byBjbHVzdGVyCnNjIDwtIHNwYXJrX2Nvbm5lY3QobWFzdGVyID0gInlhcm4tY2xpZW50IiwgY29uZmlnID0gY29uZmlnLCB2ZXJzaW9uID0gJzEuNi4xJykKYGBgCgojIyBQcmVwYXJlIHlvdXIgZGF0YQoKYGBge3IgZGF0YX0KIyBDcmVhdGUgdGFibGUgcmVmZXJlbmNlcwp0cmlwc190YmwgPC0gdGJsKHNjLCAidHJpcHNfcGFyIikKbnljdDIwMTBfdGJsIDwtIHRibChzYywgIm55Y3QyMDEwIikKCiMgSm9pbiB0YWJsZXMKdHJpcHNfam9pbmVkX3RibCA8LSB0cmlwc190YmwgJT4lCiAgZmlsdGVyKCFpcy5uYShwaWNrdXBfbnljdDIwMTBfZ2lkKSAmICFpcy5uYShkcm9wb2ZmX255Y3QyMDEwX2dpZCkpICU+JQogIHNlbGVjdChwaWNrdXBfZGF0ZXRpbWUsIGRyb3BvZmZfZGF0ZXRpbWUsCiAgICAgICAgIHBpY2t1cF9sYXRpdHVkZSwgZHJvcG9mZl9sYXRpdHVkZSwgCiAgICAgICAgIHBpY2t1cF9sb25naXR1ZGUsIGRyb3BvZmZfbG9uZ2l0dWRlLAogICAgICAgICBwaWNrdXBfbnljdDIwMTBfZ2lkLCBkcm9wb2ZmX255Y3QyMDEwX2dpZCwKICAgICAgICAgdGlwX2Ftb3VudCwgZmFyZV9hbW91bnQsIHZlbmRvcl9pZCwgcGFzc2VuZ2VyX2NvdW50LCB0cmlwX2Rpc3RhbmNlKSAlPiUKICBsZWZ0X2pvaW4oCiAgICBzZWxlY3QobnljdDIwMTBfdGJsLCBwaWNrdXBfZ2lkID0gZ2lkLCBwaWNrdXBfYm9ybyA9IGJvcm9uYW1lLCBwaWNrdXBfbnRhID0gbnRhbmFtZSksIAogICAgYnkgPSBjKCJwaWNrdXBfbnljdDIwMTBfZ2lkIiA9ICJwaWNrdXBfZ2lkIikpICU+JQogIGxlZnRfam9pbigKICAgIHNlbGVjdChueWN0MjAxMF90YmwsIGRyb3BvZmZfZ2lkID0gZ2lkLCBkcm9wb2ZmX2Jvcm8gPSBib3JvbmFtZSwgZHJvcG9mZl9udGEgPSBudGFuYW1lKSwgCiAgICBieSA9IGMoImRyb3BvZmZfbnljdDIwMTBfZ2lkIiA9ICJkcm9wb2ZmX2dpZCIpKSAlPiUKICBzZGZfcmVnaXN0ZXIoInRyaXBzX3Bhcl9qb2luZWQiKQoKIyBDYWNoZSB0YWJsZQp0YmxfY2FjaGUoc2MsICJ0cmlwc19wYXJfam9pbmVkIikKYGBgCgojIFVuZGVyc3RhbmQgeW91ciBkYXRhCgojIyBTcGFyayBTUUwgYW5kIGRwbHlyCgpVc2UgZHBseXIgc3ludGF4IHRvIHdyaXRlIFNwYXJrIFNRTC4gV2hlbiB5b3UncmUgcmVhZHkgdG8gdmlzdWFsaXplIHlvdXIgZGF0YSwgdXNlIGBjb2xsZWN0YCB0byBicmluZyBkYXRhIGludG8gUiBtZW1vcnkuIE5vdGljZSB0aGUgZGF0YSBkZXNjcmliZSByb3VnaGx5IDE3MCBtaWxsaW9uIHRyaXBzIHBlciB5ZWFyLgoKYGBge3IgY291bnRzfQojIENhbGN1bGF0ZSB0b3RhbCB0cmlwcwp0cmlwc19qb2luZWRfdGJsICU+JSBjb3VudAoKIyBDYWxjdWxhdGUgdHJpcHMgYnkgeWVhcgp0cmlwX2J5X3llYXIgPC0gdHJpcHNfam9pbmVkX3RibCAlPiUKICBtdXRhdGUoeWVhciA9IHllYXIocGlja3VwX2RhdGV0aW1lKSkgJT4lCiAgZ3JvdXBfYnkoeWVhcikgJT4lCiAgc3VtbWFyaXplKG4gPSBuKCkpICU+JQogIGNvbGxlY3QoKQoKIyBQbG90IHRyaXBzIGJ5IHllYXIKZ2dwbG90KHRyaXBfYnlfeWVhciwgYWVzKHllYXIsIG4pKSArIAogIGdlb21fYmFyKHN0YXQ9IklkZW50aXR5IikgKwogIHNjYWxlX3lfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OmNvbW1hKSArCiAgbGFicyh0aXRsZSA9ICJOdW1iZXIgb2YgdHJpcHMgYnkgeWVhciIsIHggPSAiWWVhciIsIHkgPSAiIikKYGBgCgpZb3UgY2FuIG1lYXN1cmUgdGhlIHRpbWUgYmV0d2VlbiBwaWNrdXAgYW5kIGRyb3BvZmYgZm9yIGFueSB0d28gbG9jYXRpb25zLiBJbiB0aGlzIGV4YW1wbGUsIHdlIG1lYXN1cmUgdGhlIHRpbWUgYmV0d2VlbiBKRksgYW5kIG1pZC10b3duLiBOb3RpY2UgdGhlIGxvbmdlc3QgdHJpcCB0aW1lcyBvY2N1ciBhcm91bmQgNCBQTS4KCmBgYHtyIGhvdXJseX0KIyBDYWxjbHVhdGUgdHJpcCB0aW1lIGZvciBhIHNwZWNpZmljIHBpY2t1cCBhbmQgZHJvcG9mZgpwaWNrdXBfZHJvcG9mZl90YmwgPC0gdHJpcHNfam9pbmVkX3RibCAlPiUKICBmaWx0ZXIocGlja3VwX255Y3QyMDEwX2dpZCA9PSAxMjUwICYgZHJvcG9mZl9ueWN0MjAxMF9naWQgPT0gMjA1NikgJT4lCiAgbXV0YXRlKHBpY2t1cF9ob3VyID0gaG91cihwaWNrdXBfZGF0ZXRpbWUpKSAlPiUKICBtdXRhdGUodHJpcF90aW1lID0gdW5peF90aW1lc3RhbXAoZHJvcG9mZl9kYXRldGltZSkgLSB1bml4X3RpbWVzdGFtcChwaWNrdXBfZGF0ZXRpbWUpKSAlPiUKICBncm91cF9ieShwaWNrdXBfaG91cikgJT4lIAogIHN1bW1hcml6ZShuID0gbigpLAogICAgICAgICAgICB0cmlwX3RpbWVfbWVhbiA9IG1lYW4odHJpcF90aW1lKSwKICAgICAgICAgICAgdHJpcF90aW1lX3AxMCA9IHBlcmNlbnRpbGUodHJpcF90aW1lLCAwLjEwKSwKICAgICAgICAgICAgdHJpcF90aW1lX3AyNSA9IHBlcmNlbnRpbGUodHJpcF90aW1lLCAwLjI1KSwKICAgICAgICAgICAgdHJpcF90aW1lX3A1MCA9IHBlcmNlbnRpbGUodHJpcF90aW1lLCAwLjUwKSwKICAgICAgICAgICAgdHJpcF90aW1lX3A3NSA9IHBlcmNlbnRpbGUodHJpcF90aW1lLCAwLjc1KSwKICAgICAgICAgICAgdHJpcF90aW1lX3A5MCA9IHBlcmNlbnRpbGUodHJpcF90aW1lLCAwLjkwKSkKCiMgQ29sbGVjdCByZXN1bHRzCnBpY2t1cF9kcm9wb2ZmIDwtIGNvbGxlY3QocGlja3VwX2Ryb3BvZmZfdGJsKQoKIyBQbG90CmdncGxvdChwaWNrdXBfZHJvcG9mZiwgYWVzKHggPSBwaWNrdXBfaG91cikpICsKICAgICAgICAgIGdlb21fbGluZShhZXMoeSA9IHRyaXBfdGltZV9wNTAsIGFscGhhID0gIk1lZGlhbiIpKSArCiAgICAgICAgICBnZW9tX3JpYmJvbihhZXMoeW1pbiA9IHRyaXBfdGltZV9wMjUsIHltYXggPSB0cmlwX3RpbWVfcDc1LCAKICAgICAgICAgICAgICAgICAgICAgICAgICBhbHBoYSA9ICIyNeKAkzc1dGggcGVyY2VudGlsZSIpKSArCiAgICAgICAgICBnZW9tX3JpYmJvbihhZXMoeW1pbiA9IHRyaXBfdGltZV9wMTAsIHltYXggPSB0cmlwX3RpbWVfcDkwLCAKICAgICAgICAgICAgICAgICAgICAgICAgICBhbHBoYSA9ICIxMOKAkzkwdGggcGVyY2VudGlsZSIpKSArCiAgICAgICAgICBzY2FsZV95X2NvbnRpbnVvdXMoInRyaXAgZHVyYXRpb24gaW4gbWludXRlcyIpCgpgYGAKCiMjIFNwYXJrIE1MCgpXaXRoIFNwYXJrIE1MIHlvdSBjYW4gcnVuIG1hY2hpbmUgbGVhcm5pbmcgYWxnb3JpdGhtcyBhZ2FpbnN0IGFsbCB5b3VyIGRhdGEgaW4gU3BhcmsuIEhlcmUgd2UgYXR0ZW1wdCB0byB1bmRlcnN0YW5kIHdoYXQgZmFjdG9ycyBpbmZsdWVuY2UgdGlwIGFtb3VudHMuCgpgYGB7ciBtb2RlbH0KIyBTZWxlY3QgYSBtb2RlbCBkYXRhIHNldAptb2RlbF90YmwgPC0gdHJpcHNfam9pbmVkX3RibCAlPiUKICBmaWx0ZXIocGlja3VwX255Y3QyMDEwX2dpZCA9PSAxMjUwICYgZHJvcG9mZl9ueWN0MjAxMF9naWQgPT0gMjA1NikgJT4lCiAgbXV0YXRlKHBpY2t1cF9ob3VyID0gaG91cihwaWNrdXBfZGF0ZXRpbWUpKSAlPiUKICBtdXRhdGUocGlja3VwX3dlZWsgPSB3ZWVrb2Z5ZWFyKHBpY2t1cF9kYXRldGltZSkpICU+JQogIG11dGF0ZShwaWNrdXBfeWVhciA9IHllYXIocGlja3VwX2RhdGV0aW1lKSkgJT4lCiAgbXV0YXRlKHRyaXBfdGltZSA9IHVuaXhfdGltZXN0YW1wKGRyb3BvZmZfZGF0ZXRpbWUpIC0gdW5peF90aW1lc3RhbXAocGlja3VwX2RhdGV0aW1lKSkgJT4lCiAgZmlsdGVyKCFpcy5uYShwaWNrdXBfbnljdDIwMTBfZ2lkKSAmICFpcy5uYShkcm9wb2ZmX255Y3QyMDEwX2dpZCkpICU+JQogIGZpbHRlcighaXMubmEodGlwX2Ftb3VudCkpICU+JQogIGZpbHRlcighaXMubmEoZmFyZV9hbW91bnQpKSAlPiUKICBmaWx0ZXIoIWlzLm5hKHZlbmRvcl9pZCkpICU+JQogIGZpbHRlcighaXMubmEocGlja3VwX2hvdXIpKSAlPiUKICBmaWx0ZXIoIWlzLm5hKHBpY2t1cF93ZWVrKSkgJT4lCiAgZmlsdGVyKCFpcy5uYShwYXNzZW5nZXJfY291bnQpKSAlPiUKICBmaWx0ZXIoIWlzLm5hKHRyaXBfdGltZSkpICU+JQogIGZpbHRlcighaXMubmEodHJpcF9kaXN0YW5jZSkpCgojIFBhcnRpdGlvaW4gaW50byB0cmFpbiBhbmQgdmFsaWRhdGUKbW9kZWxfcGFydGl0aW9uX3RibCA8LSBtb2RlbF90YmwgJT4lCiAgc2RmX3BhcnRpdGlvbih0cmFpbiA9IDAuOCwgdGVzdCA9IDAuMiwgc2VlZCA9IDEyMzQpCgojIENyZWF0ZSB0YWJsZSByZWZlcmVuY2VzCnRyaXBzX3RyYWluX3RibCA8LSBzZGZfcmVnaXN0ZXIobW9kZWxfcGFydGl0aW9uX3RibCR0cmFpbiwgInRyaXBzX3RyYWluIikKdHJpcHNfdGVzdF90YmwgPC0gc2RmX3JlZ2lzdGVyKG1vZGVsX3BhcnRpdGlvbl90YmwkdHJhaW4sICJ0cmlwc190ZXN0IikKCiMgQ2FjaGUgbW9kZWwgZGF0YQp0YmxfY2FjaGUoc2MsICJ0cmlwc190cmFpbiIpCgojIE1vZGVsIGRhdGEKbW9kZWxfZm9ybXVsYSA8LSBmb3JtdWxhKHRpcF9hbW91bnQgfiAKICAgICAgICAgICAgICAgICAgICAgICAgICAgZmFyZV9hbW91bnQgKyB2ZW5kb3JfaWQgKyBwaWNrdXBfaG91ciArIHBpY2t1cF93ZWVrICsgCiAgICAgICAgICAgICAgICAgICAgICAgICAgIHBhc3Nlbmdlcl9jb3VudCArIHRyaXBfdGltZSArIHRyaXBfZGlzdGFuY2UpCm0xIDwtIG1sX2xpbmVhcl9yZWdyZXNzaW9uKHRyaXBzX3RyYWluX3RibCwgbW9kZWxfZm9ybXVsYSkKc3VtbWFyeShtMSkKYGBgCgojIyBIVE1MIFdpZGdldHMKClNwYXJrIG1ha2VzIGl0IGVhc3kgdG8gZHJpbGwgZG93biB0byBhbnkgbGV2ZWwgb2YgeW91IGRhdGEuIFlvdSBjYW4gdmlzdWFsaXplIHRoZSB0b3AgZHJvcG9mZnMgZm9yIGFueSBwaWNrdXAgbG9jYXRpb24gYnkgZW50ZXJpbmcgeW91ciBwaWNrdXAgbG9jYXRpb24uIEluIHRoaXMgZXhhbXBsZSB0aGUgcGlja3VwIGxvY2F0aW9uIGlzIEpGSyBJbnRlcm5hdGlvbmFsIEFpcnBvcnQuCgpgYGB7ciB0b3Bkcm9wb2Zmc30KIyBTdW1tYXJpemUgdHJpcHMgZnJvbSBKRksgQWlycG9ydApqZmtfcGlja3VwX3RibCA8LSB0cmlwc19qb2luZWRfdGJsICU+JQogIGZpbHRlcihwaWNrdXBfbnljdDIwMTBfZ2lkID09IDIwNTYpICU+JQogIGZpbHRlcighaXMubmEoZHJvcG9mZl9ueWN0MjAxMF9naWQpKSAlPiUKICBtdXRhdGUodHJpcF90aW1lID0gdW5peF90aW1lc3RhbXAoZHJvcG9mZl9kYXRldGltZSkgLSB1bml4X3RpbWVzdGFtcChwaWNrdXBfZGF0ZXRpbWUpKSAlPiUKICBncm91cF9ieShkcm9wb2ZmX255Y3QyMDEwX2dpZCkgJT4lIAogIHN1bW1hcml6ZShuID0gbigpLAogICAgICAgICAgICB0cmlwX3RpbWVfbWVhbiA9IG1lYW4odHJpcF90aW1lKSwKICAgICAgICAgICAgdHJpcF9kaXN0X21lYW4gPSBtZWFuKHRyaXBfZGlzdGFuY2UpLAogICAgICAgICAgICBkcm9wb2ZmX2xhdGl0dWRlID0gbWVhbihkcm9wb2ZmX2xhdGl0dWRlKSwKICAgICAgICAgICAgZHJvcG9mZl9sb25naXR1ZGUgPSBtZWFuKGRyb3BvZmZfbG9uZ2l0dWRlKSwKICAgICAgICAgICAgcGFzc2VuZ2VyX21lYW4gPSBtZWFuKHBhc3Nlbmdlcl9jb3VudCksCiAgICAgICAgICAgIGZhcmVfYW1vdW50ID0gbWVhbihmYXJlX2Ftb3VudCksCiAgICAgICAgICAgIHRpcF9hbW91bnQgPSBtZWFuKHRpcF9hbW91bnQpKQoKIyBDb2xsZWN0IHRvcCByZXN1bHRzCmpma19waWNrdXAgPC0gamZrX3BpY2t1cF90YmwgJT4lCiAgbXV0YXRlKG5fcmFuayA9IG1pbl9yYW5rKGRlc2MobikpKSAlPiUKICBmaWx0ZXIobl9yYW5rIDw9IDI1KSAlPiUKICBjb2xsZWN0CgojIFBsb3QgdG9wIHRyaXBzIG9uIG1hcApsZWFmbGV0KGpma19waWNrdXApICU+JSAKICBzZXRWaWV3KGxuZyA9IC03My45LCBsYXQgPSA0MC43LCB6b29tID0gMTEpICU+JQogIGFkZFByb3ZpZGVyVGlsZXMoIkNhcnRvREIuUG9zaXRyb24iKSAlPiUKICBhZGRDaXJjbGVNYXJrZXJzKH5kcm9wb2ZmX2xvbmdpdHVkZSwgfmRyb3BvZmZfbGF0aXR1ZGUsIHN0cm9rZSA9IEYsIGNvbG9yID0gInJlZCIpICU+JQogIGFkZENpcmNsZU1hcmtlcnMoLTczLjc3ODEsIDQwLjY0MTMsIGZpbGwgPSBGQUxTRSwgY29sb3IgPSAiZ3JlZW4iKQpgYGAKCiMjIFNoaW55IEdhZGdldAoKSXQgaXMgb2Z0ZW4gdXNlZnVsIHRvIHZpc3VhbGl6ZSB0aGUgYW5hbHlzZXMgaW50ZXJhY3RpdmVseS4gVXNlIGEgU2hpbnkgZ2FkZ2V0IHRvIGV4cGxvcmUgb3RoZXIgcGlja3VwIGFuZCBkcm9wb2ZmIGxvY2F0aW9ucy4KCmBgYHtyIGdhZGdldCwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KIyBDcmVhdGUgbGlzdCBvZiBnZW8gZ3JvdXBzIHRvIHNlbGVjdCBmcm9tCmdlb19ncm91cCA8LSB0cmlwc19qb2luZWRfdGJsICU+JSAKICBkaXN0aW5jdChwaWNrdXBfbnRhKSAlPiUKICBhcnJhbmdlKHBpY2t1cF9udGEpICU+JQogIGNvbGxlY3QKCiMgQ3JlYXRlIHRoZSBnYWRnZXQgdXNlciBpbnRlcmZhY2UKdWkgPC0gbWluaVBhZ2UoCiAgZ2FkZ2V0VGl0bGVCYXIoIk5ZQyBUYXhpIFRyaXBzIiksCiAgbWluaVRhYnN0cmlwUGFuZWwoCiAgICBtaW5pVGFiUGFuZWwoIklucHV0cyIsIGljb24gPSBpY29uKCJzbGlkZXJzIiksCiAgICAgICAgICAgICAgICAgbWluaUNvbnRlbnRQYW5lbCgKICAgICAgICAgICAgICAgICAgIHNlbGVjdElucHV0KCJwaWNrdXAiLCAgIlRheGkgUGlja3VwIiwgZ2VvX2dyb3VwLCAiTGluY29sbiBTcXVhcmUiKSwKICAgICAgICAgICAgICAgICAgIHNlbGVjdElucHV0KCJkcm9wb2ZmIiwgICJUYXhpIERyb3BvZmYiLCBnZW9fZ3JvdXAsICJVcHBlciBXZXN0IFNpZGUiKQogICAgICAgICAgICAgICAgICAgKQogICAgICAgICAgICAgICAgICksCiAgICBtaW5pVGFiUGFuZWwoIlBsb3QiLCBpY29uID0gaWNvbigiYXJlYS1jaGFydCIpLAogICAgICAgICAgICAgICAgIG1pbmlDb250ZW50UGFuZWwoCiAgICAgICAgICAgICAgICAgICBwbG90T3V0cHV0KCJ0cmlwVGltZVBsb3QiKQogICAgICAgICAgICAgICAgICAgKQogICAgKSwKICAgIG1pbmlUYWJQYW5lbCgiTWFwIiwgaWNvbiA9IGljb24oIm1hcC1vIiksCiAgICAgICAgICAgICAgICAgbWluaUNvbnRlbnRQYW5lbCgKICAgICAgICAgICAgICAgICAgIGxlYWZsZXRPdXRwdXQoInRyaXBMZWFmbGV0IikKICAgICAgICAgICAgICAgICApCiAgICApLAogICAgbWluaVRhYlBhbmVsKCJEYXRhIiwgaWNvbiA9IGljb24oInRhYmxlIiksCiAgICAgICAgICAgICAgICAgbWluaUNvbnRlbnRQYW5lbCgKICAgICAgICAgICAgICAgICAgIGRhdGFUYWJsZU91dHB1dCgidGFibGUiLCBoZWlnaHQgPSAiMTAwJSIpCiAgICAgICAgICAgICAgICAgKQogICAgKQogICkKKQoKIyBDcmVhdGUgdGhlIHNoaW55IGdhZGdldCBmdW5jdGlvbnMKc2VydmVyIDwtIGZ1bmN0aW9uKGlucHV0LCBvdXRwdXQpIHsKCiAgc2hpbnlfcGlja3VwX2Ryb3BvZmZfaG91ciA8LSByZWFjdGl2ZSh7CiAgICB0cmlwc19qb2luZWRfdGJsICU+JQogICAgZmlsdGVyKHBpY2t1cF9udGEgPT0gaW5wdXQkcGlja3VwICYgZHJvcG9mZl9udGEgPT0gaW5wdXQkZHJvcG9mZikgJT4lCiAgICBtdXRhdGUocGlja3VwX2hvdXIgPSBob3VyKHBpY2t1cF9kYXRldGltZSkpICU+JQogICAgbXV0YXRlKHRyaXBfdGltZSA9IHVuaXhfdGltZXN0YW1wKGRyb3BvZmZfZGF0ZXRpbWUpIC0gdW5peF90aW1lc3RhbXAocGlja3VwX2RhdGV0aW1lKSkgJT4lCiAgICBncm91cF9ieShwaWNrdXBfaG91cikgJT4lIAogICAgc3VtbWFyaXplKG4gPSBuKCksCiAgICAgICAgICAgICAgcGlja3VwX2xhdGl0dWRlID0gbWVhbihwaWNrdXBfbGF0aXR1ZGUpLAogICAgICAgICAgICAgIHBpY2t1cF9sb25naXR1ZGUgPSBtZWFuKHBpY2t1cF9sb25naXR1ZGUpLAogICAgICAgICAgICAgIGRyb3BvZmZfbGF0aXR1ZGUgPSBtZWFuKGRyb3BvZmZfbGF0aXR1ZGUpLAogICAgICAgICAgICAgIGRyb3BvZmZfbG9uZ2l0dWRlID0gbWVhbihkcm9wb2ZmX2xvbmdpdHVkZSksCiAgICAgICAgICAgICAgdHJpcF90aW1lX21lYW4gPSBtZWFuKHRyaXBfdGltZSksCiAgICAgICAgICAgICAgdHJpcF90aW1lX3AxMCA9IHBlcmNlbnRpbGUodHJpcF90aW1lLCAwLjEwKSwKICAgICAgICAgICAgICB0cmlwX3RpbWVfcDI1ID0gcGVyY2VudGlsZSh0cmlwX3RpbWUsIDAuMjUpLAogICAgICAgICAgICAgIHRyaXBfdGltZV9wNTAgPSBwZXJjZW50aWxlKHRyaXBfdGltZSwgMC41MCksCiAgICAgICAgICAgICAgdHJpcF90aW1lX3A3NSA9IHBlcmNlbnRpbGUodHJpcF90aW1lLCAwLjc1KSwKICAgICAgICAgICAgICB0cmlwX3RpbWVfcDkwID0gcGVyY2VudGlsZSh0cmlwX3RpbWUsIDAuOTApKSAlPiUKICAgIGNvbGxlY3QKICB9KQoKICBzaGlueV9waWNrdXBfZHJvcG9mZiA8LSByZWFjdGl2ZSh7CiAgICBzaGlueV9waWNrdXBfZHJvcG9mZl9ob3VyKCkgJT4lCiAgICAgIHN1bW1hcml6ZShuID0gbigpLAogICAgICAgICAgICAgICAgcGlja3VwX2xhdGl0dWRlID0gbWVhbihwaWNrdXBfbGF0aXR1ZGUpLAogICAgICAgICAgICAgICAgcGlja3VwX2xvbmdpdHVkZSA9IG1lYW4ocGlja3VwX2xvbmdpdHVkZSksCiAgICAgICAgICAgICAgICBkcm9wb2ZmX2xhdGl0dWRlID0gbWVhbihkcm9wb2ZmX2xhdGl0dWRlKSwKICAgICAgICAgICAgICAgIGRyb3BvZmZfbG9uZ2l0dWRlID0gbWVhbihkcm9wb2ZmX2xvbmdpdHVkZSkpCiAgfSkKICAKICBvdXRwdXQkdHJpcFRpbWVQbG90IDwtIHJlbmRlclBsb3QoewogICAgZ2dwbG90KHNoaW55X3BpY2t1cF9kcm9wb2ZmX2hvdXIoKSwgYWVzKHggPSBwaWNrdXBfaG91cikpICsKICAgIGdlb21fbGluZShhZXMoeSA9IHRyaXBfdGltZV9wNTAgLyA2MCwgYWxwaGEgPSAiTWVkaWFuIikpICsKICAgIGdlb21fcmliYm9uKGFlcyh5bWluID0gdHJpcF90aW1lX3AyNSAvIDYwLCAKICAgICAgICAgICAgICAgICAgICB5bWF4ID0gdHJpcF90aW1lX3A3NSAvIDYwLCAKICAgICAgICAgICAgICAgICAgICBhbHBoYSA9ICIyNeKAkzc1dGggcGVyY2VudGlsZSIpKSArCiAgICBnZW9tX3JpYmJvbihhZXMoeW1pbiA9IHRyaXBfdGltZV9wMTAgLyA2MCwgCiAgICAgICAgICAgICAgICAgICAgeW1heCA9IHRyaXBfdGltZV9wOTAgLyA2MCwgCiAgICAgICAgICAgICAgICAgICAgYWxwaGEgPSAiMTDigJM5MHRoIHBlcmNlbnRpbGUiKSkgKwogICAgc2NhbGVfeV9jb250aW51b3VzKCJ0cmlwIGR1cmF0aW9uIGluIG1pbnV0ZXMiKSArIAogICAgZ2d0aXRsZSgiVHJpcCB0aW1lIGluIG1pbnV0ZXMiKQogICB9KQoKICBvdXRwdXQkdHJpcExlYWZsZXQgPC0gcmVuZGVyTGVhZmxldCh7ICAKICAgICAgbGVhZmxldChzaGlueV9waWNrdXBfZHJvcG9mZigpKSAlPiUgCiAgICAgIGFkZFByb3ZpZGVyVGlsZXMoIkNhcnRvREIuUG9zaXRyb24iKSAlPiUKICAgICAgYWRkQ2lyY2xlTWFya2Vycyh+cGlja3VwX2xvbmdpdHVkZSwgfnBpY2t1cF9sYXRpdHVkZSwgZmlsbCA9IEZBTFNFLCBjb2xvciA9ICJncmVlbiIpICU+JQogICAgICBhZGRDaXJjbGVNYXJrZXJzKH5kcm9wb2ZmX2xvbmdpdHVkZSwgfmRyb3BvZmZfbGF0aXR1ZGUsIHN0cm9rZSA9IEZBTFNFLCBjb2xvciA9ICJyZWQiKQogIH0pCiAgCiAgb3V0cHV0JHRhYmxlIDwtIHJlbmRlckRhdGFUYWJsZSh7CiAgICBzaGlueV9waWNrdXBfZHJvcG9mZl9ob3VyKCkgJT4lCiAgICAgIG11dGF0ZSh0cmlwX3RpbWVfbWVhbiA9IHJvdW5kKHRyaXBfdGltZV9tZWFuIC8gNjApKSAlPiUKICAgICAgbXV0YXRlKHRyaXBfdGltZV9wNTAgPSByb3VuZCh0cmlwX3RpbWVfcDUwIC8gNjApKSAlPiUKICAgICAgc2VsZWN0KHBpY2t1cF9ob3VyLCBuLCB0cmlwX3RpbWVfbWVhbiwgdHJpcF90aW1lX3A1MCkKICB9KQogIAogIG9ic2VydmVFdmVudChpbnB1dCRkb25lLCB7CiAgICBzdG9wQXBwKFRSVUUpCiAgfSkKCn0KCiMgUnVuIHRoZSBnYWRnZXQKcnVuR2FkZ2V0KHVpLCBzZXJ2ZXIpCmBgYAoKIyBDb21tdW5pY2F0ZSB5b3VyIGluc2lnaHRzCgpXZSBlc3RpbWF0ZWQgdGF4aSB0cmlwIHRpbWVzIGJ5IHRpbWUgb2YgZGF5IGZvciBtdWx0aXBsZSBwaWNrdXAgYW5kIGRyb3BvZmYgbG9jYXRpb25zLiBXZSBmb3VuZCB0aGUga2V5IGRyaXZlcnMgZm9yIHByZWRpY3RpbmcgdGlwIGFtb3VudHMgZm9yIHNwZWNpZmljIHBpY2t1cCBhbmQgZHJvcG9mZiBsb2NhdGlvbnMuIFdlIGFsc28gZm91bmQgdGhlIHRvcCBkcm9wb2ZmIGxvY2F0aW9ucyBmb3Igc3BlY2lmaWMgcGlja3VwIGxvY2F0aW9ucy4gRmluYWxseSwgd2UgcHVibGlzaGVkIHRoZXNlIHJlc3VsdHMgaW4gYW4gUiBtYXJrZG93biBub3RlYm9vay4KCg==