Introduction

This tutorial broadly covers validation approaches for low-cost PM2.5 sensor data. It can be distilled into three main sections:

  1. Downloading sample data from the Purple Air Network
  2. Assessing this data based on the metrics outlined in the 2021 U.S. EPA Base Testing Report (Duvall et al. 2021)
  3. Considering some approaches for calibration models and assessing their performance

This tutorial is a companion to the Journal of Aerosol Science article “Tutorial: Guidelines for Implementing Low-Cost Sensor Networks for Aerosol Monitoring.” When using this data or code please cite the corresponding paper:

N. Zimmerman, (2021). “Tutorial: Guidelines for Implementing Low-Cost Sensor Networks for Aerosol Monitoring.” Journal of Aerosol Science, in press. (https://doi.org/10.1016/j.jaerosci.2021.105872)

One can also consider citing this web app directly:

N. Zimmerman. “Low-Cost PM Sensor Tutorial.” (https://nzimmerman-ubc.github.io/lcs_PM_demo/).

In the top-right corner of this page is a button labeled “Code” which will allow you to download the R Markdown file used to generate this project. To run the code on your own machine, you will need to download and install both RStudio (RStudio Team 2020) and the package “rmarkdown” (Allaire et al. 2020). See here to learn more.

Required packages

Before beginning, we will need to install a number of packages to make this code run (Galili 2019; James and Hornik 2020; Gerds 2019; Warnes, Bolker, and Lumley 2020; Callahan, Martin, Wilson, et al. 2020; Kuhn 2020; Dowle and Srinivasan 2020; Wickham 2019; Carslaw and Ropkins 2020; Zhu 2020; Robinson, Hayes, and Couch 2020; LaZerte 2021; Muggeo 2021). This code chunk will check if you have these packages installed. If you do not have them installed, it will install them. If they are already installed, the code will load them.

if (!require(installr)) install.packages('installr')
library(installr)

if (!require(tidyverse)) install.packages('tidyverse')
library(tidyverse)

if (!require(chron)) install.packages('chron')
library(chron)

if (!require(prodlim)) install.packages('prodlim')
library(prodlim)

if (!require(gtools)) install.packages('gtools')
library(gtools)

if (!require(AirSensor)) install.packages('AirSensor')
library(AirSensor)

if (!require(caret)) install.packages('caret')
library(caret)

if (!require(data.table)) install.packages('data.table')
library(data.table)

if (!require(openair)) install.packages('openair')
library(openair)

if (!require(kableExtra)) install.packages('kableExtra')
library(kableExtra)

if (!require(broom)) install.packages('broom')
library(broom)

if (!require(weathercan)) install.packages('weathercan')
library(weathercan)

if (!require(segmented)) install.packages('segmented')
library(segmented)

Part 1: Downloading Sample Data

To illustrate how to work with low-cost PM sensor data, we will take advantage of a large, established network: PurpleAir. The tutorial will consider some sensors deployed in the Greater Vancouver Area, but this code could be easily adopted to different PurpleAir sensors by adjusting the sensor identifier.

Using the PurpleAir network in this tutorial is convenient, since the R Package “AirSensor” (Callahan, Martin, Wilson, et al. 2020) has developed easily implementable tools for downloading PurpleAir data from their network. The development of the AirSensor package is also detailed by Feenstra et al. (Feenstra et al. 2020).

Set up a local directory

The AirSensor package needs to know where processed data will live. For this report, we will specify a local archiveBaseDir where downloaded and processed data will live. The following code specifies a local directory archive, checks for its existence and creates it if needed. It also checks to see if you have downloaded the required spatial data sets (to extract PurpleAir data by Country Code) and installs them if necessary. This chunk of code was developed based on the Article “Australia on Fire” by Mazama Science.

## Create a folder in your R home directory for this tutorial called 'PM_Sensor_Tutorial'
new_wd <- path.expand("~/PM_Sensor_Tutorial")
if ( !dir.exists(new_wd) ) {
  dir.create(new_wd)
}
setwd(new_wd)

## Create a folder in 'PM_Sensor_Tutorial' for storing the downloaded PurpleAir data.
archiveBaseDir <- path.expand("~/PM_Sensor_Tutorial/Data")
if ( !dir.exists(archiveBaseDir) ) {
  dir.create(archiveBaseDir)
}
setArchiveBaseDir(archiveBaseDir)

## Checking to see if you have the required spatial data, and installing it un a sub-folder of PM_Sensor_Tutorial/Data/Spatial if needed
if (!require(MazamaSpatialUtils)) install.packages('MazamaSpatialUtils')
library(MazamaSpatialUtils)

if ( !dir.exists('~/PM_Sensor_Tutorial/Data/Spatial') ) {
  dir.create('~/PM_Sensor_Tutorial/Data/Spatial')
  setSpatialDataDir('~/PM_Sensor_Tutorial/Data/Spatial')
  installSpatialData()
  installSpatialData('NaturalEarthAdm1')
}

Load synoptic data for Canada

We will use the AirSensor pas_createNew() function to create a PurpleAir Synoptic (pas) object containing all the spatial metadata associated with PurpleAir monitors in Canada (we will call this object ‘pas_CAN’). See this Mazama Science page for more details.

The following code checks to make sure the pas_CAN file exists in your archiveBaseDir directory and, if missing, recreates it.

filePath_pas <- file.path(archiveBaseDir, "pas_CAN.rda")

if ( !file.exists(filePath_pas) ) {
  initializeMazamaSpatialUtils(spatialDataDir = "~/PM_Sensor_Tutorial/Data/Spatial")
  pas_CAN <- pas_createNew(countryCodes = "CA", includePWFSL = TRUE, lookbackDays = 1,baseUrl = "https://www.purpleair.com/json?all=true")
  save(pas_CAN, file = filePath_pas)
}

pas_CAN <- get(load(filePath_pas))

Visualize snapshot of Air Quality in Canada

Now we will print a map of the results. This is all the PurpleAir units currently online in Canada. If you click on a marker, it will pop up the monitor details (most recent reading, station name, etc.). The markers are coloured by Air Quality Index (AQI).

pas_leaflet(pas_CAN)

Download our Sample Data

Metro Vancouver, the local regulatory agency, has two PurpleAir monitors co-located with their ‘Burnaby South’ Regulatory Monitoring Site [https://gis.metrovancouver.org/maps/Air]. This is a good site for our tutorial, as there is both regulatory data and low-cost sensor data available. The U.S. EPA recommends that when assessing sensor performance, a minimum of three co-located sensors be assessed (Duvall et al. 2021). While this is not exactly possible here, we will also scrape data from an additional PurpleAir monitor located approximately 1 km east of the Metro Vancouver Burnaby South Regulatory Monitoring Station. We will store this data as PurpleAir Time Series (pat) objects.

Summary PurpleAir monitor locations and naming convention used here:

  • pat_Burnaby1: PurpleAir ID: “NAPSID: 100119 - 1,” located at: Burnaby South Regulatory Monitoring Station.
  • pat_Burnaby2: PurpleAir ID: “NAPSID: 100119 - 2,” located at: Burnaby South Regulatory Monitoring Station.
  • pat_Burnaby3: PurpleAir ID: “NAPSID: 100119 - 3,” located: ~ 1 km east of the Burnaby South Regulatory Monitoring Station.

We will download data from September 1 2020 - December 31 2020 (4 months - including a wildfire smoke episode).

filePath_Burnaby1 <- file.path(archiveBaseDir, "pat_Burnaby1.rda")

if ( file.exists(filePath_Burnaby1) ) {
  pat_Burnaby1 <- get(load(filePath_Burnaby1))
} else {
  pat_Burnaby1 <- pat_createNew(
    pas = pas_CAN, 
    label = "NAPSID: 100119 - 1", 
    startdate = 20200901, 
    enddate = 20201231
  )
  save(pat_Burnaby1, file = filePath_Burnaby1)
}

filePath_Burnaby2 <- file.path(archiveBaseDir, "pat_Burnaby2.rda")

if ( file.exists(filePath_Burnaby2) ) {
  pat_Burnaby2 <- get(load(filePath_Burnaby2))
} else {
  pat_Burnaby2 <- pat_createNew(
    pas = pas_CAN, 
    label = "NAPSID: 100119-2", 
    startdate = 20200901, 
    enddate = 20201231
  )
  save(pat_Burnaby2, file = filePath_Burnaby2)
}

filePath_Burnaby3 <- file.path(archiveBaseDir, "pat_Burnaby3.rda")

if ( file.exists(filePath_Burnaby3) ) {
  pat_Burnaby3 <- get(load(filePath_Burnaby3))
} else {
  pat_Burnaby3 <- pat_createNew(
    pas = pas_CAN, 
    label = "NAPSID: 100119-3", 
    startdate = 20200901, 
    enddate = 20201231
  )
  save(pat_Burnaby3, file = filePath_Burnaby3)
}

Map of PurpleAir monitors

To visualize the location of these PurpleAir monitors, we can use the “pas_leaflet” function again. Note that you can see the two PurpleAir sensors on the roof of the Burnaby South station if you zoom in.

Built-in validation in “AirSensor”

Built into the AirSensor package is a validation function, “pat_externalFit.” This function identifies the nearest regulatory monitoring data via the USFS Pacific Wildland Fire Sciences Lab’s (PWFSL) PWFSLSmoke package (Callahan, Martin, Pease, et al. 2020). Sources of regulatory PM2.5 data made accessible by the package include: data from the United States Environmental Protection Agency (US EPA) and its AirNow air quality site, AIRSIS (password protected), the Western Regional Climate Center (WRCC) and the open source site OpenAQ. THe “pat_externalFit” function creates a time series and linear regression plot of the regulatory data vs. the PurpleAir data at a 1 hour time resolution. Below is an example built-in validation of the “Burnaby1” sensor.

validation <- pat_externalFit(pat_Burnaby1, showPlot = TRUE)

Extract data for Part 2.

While the built-in validation from AirSensor is a good start, we are interested in digging deeper into the data and its performance. Before we can do this, we need to store the Regulatory Monitoring data (short form: FRM/FEM data; FRM = Federal Reference Method, FEM = Federal Equivalent Method). We will extract the following:

  • 1 h average data from all three PurpleAir monitors (“Burnaby1,” “Burnaby2,” “Burnaby3”)

  • 1 h average from the closest regulatory monitor (the Metro Vancouver Burnaby South station)

  • 24 h average data from all three PurpleAir monitors (“Burnaby1,” “Burnaby2,” “Burnaby3”)

  • 24 h average data from the closest regulatory monitor (the Metro Vancouver Burnaby South station)

There are a few data cleaning and organzing steps, but the output is two data frames:

  1. “df_1h”: a data frame with all of the 1 h averaged data
  2. “df_24h”: a data frame with all of the 24 hour averaged data

Let’s start with the hourly data:

# Get all Purple Air Sensor from Burnaby South # 1
hourlyData1 <- 
  pat_Burnaby1 %>%
  pat_aggregate() %>%
  pat_extractData()

# Get all Purple Air Sensor from Burnaby South # 2
hourlyData2 <- 
  pat_Burnaby2 %>%
  pat_aggregate() %>%
  pat_extractData()

# Get all Purple Air Sensor from Gilley Ave (1 km East)
hourlyData3 <- 
  pat_Burnaby3 %>%
  pat_aggregate() %>%
  pat_extractData()

# Add AB channel (how PurpleAir reports concentrations) and Pull Relevant Data for Analysis
hourlyData1 <- mutate(hourlyData1,pm25_AB = (pm25_A + pm25_B)/2)
data_1h_Burnaby1 <- pull(hourlyData1,pm25_AB)


hourlyData2 <- mutate(hourlyData2,pm25_AB = (pm25_A + pm25_B)/2)
data_1h_Burnaby2 <- pull(hourlyData2,pm25_AB)

hourlyData3 <- mutate(hourlyData3,pm25_AB = (pm25_A + pm25_B)/2)
data_1h_Burnaby3 <- pull(hourlyData3,pm25_AB)


# Get the PWFSL monitor data (Regulatory Data)

monitorID <- pat_Burnaby1$meta$pwfsl_closestMonitorID
tlim <- range(hourlyData1$datetime)

pwfsl_monitor <-
    PWFSLSmoke::monitor_load(tlim[1], tlim[2], monitorIDs = monitorID) %>%
    PWFSLSmoke::monitor_subset(tlim = tlim)

pwfsl_data_1h <-
    pwfsl_monitor %>%
    PWFSLSmoke::monitor_extractData()

names(pwfsl_data_1h) <- c("date", "pwfsl_pm25")
data_1h_FEM <- pull(pwfsl_data_1h,pwfsl_pm25)
datetime_1h <- pull(pwfsl_data_1h,date)


# Combine all data into a data frame

df_1h <- tibble(datetime_1h,data_1h_FEM,data_1h_Burnaby1,data_1h_Burnaby2,data_1h_Burnaby3)
df_1h <- gather(df_1h,Location,data_1h,-datetime_1h)
df_1h$Location = df_1h$Location %>% fct_relabel(str_replace, "data_1h_", "")

Now we will organize the 24 h averaged data:

# Get all Purple Air Sensor from Burnaby South # 1
dailyData1 <- 
  pat_Burnaby1 %>%
  pat_aggregate(unit="hours",count=24) %>%
  pat_extractData()

# Get all Purple Air Sensor from Burnaby South # 2
dailyData2 <- 
  pat_Burnaby2 %>%
  pat_aggregate(unit="hours",count=24) %>%
  pat_extractData()

# Get all Purple Air Sensor from Gilley Ave (1 km East)
dailyData3 <- 
  pat_Burnaby3 %>%
  pat_aggregate(unit="hours",count=24) %>%
  pat_extractData()

# Add AB channel (how PurpleAir reports concentrations) and Pull Relevant Data for Analysis
dailyData1 <- mutate(dailyData1,pm25_AB = (pm25_A + pm25_B)/2)
data_24h_Burnaby1 <- pull(dailyData1,pm25_AB)


dailyData2 <- mutate(dailyData2,pm25_AB = (pm25_A + pm25_B)/2)
data_24h_Burnaby2 <- pull(dailyData2,pm25_AB)

dailyData3 <- mutate(dailyData3,pm25_AB = (pm25_A + pm25_B)/2)
data_24h_Burnaby3 <- pull(dailyData3,pm25_AB)


# Get the PWFSL monitor data (Regulatory Data)
monitorID <- pat_Burnaby1$meta$pwfsl_closestMonitorID
tlim <- range(dailyData1$datetime)

pwfsl_monitor <-
    PWFSLSmoke::monitor_load(tlim[1], tlim[2], monitorIDs = monitorID) %>%
    PWFSLSmoke::monitor_subset(tlim = tlim)

pwfsl_data_24h <-
    pwfsl_monitor %>%
    PWFSLSmoke::monitor_extractData()

names(pwfsl_data_24h) <- c("date", "pwfsl_pm25")
pwfsl_data_24h <- timeAverage(pwfsl_data_24h,period="1 day")

data_24h_FEM <- pull(pwfsl_data_24h,pwfsl_pm25)
datetime_24h <- pull(pwfsl_data_24h,date)


# Combine all data into a data frame

df_24h <- tibble(datetime_24h,data_24h_FEM,data_24h_Burnaby1,data_24h_Burnaby2,data_24h_Burnaby3)
df_24h <- gather(df_24h,Location,data_24h,-datetime_24h)
df_24h$Location = df_24h$Location %>% fct_relabel(str_replace, "data_24h_", "")

Now we have everything we need to proceed to Part 2 of this tutorial (assessing sensor performance according to new U.S. EPA Base Testing Protocols).

Part 2: U.S. EPA Base Testing Metrics

When the U.S. EPA released their performance testing protocols for fine particulate matter sensors, they also released a “Fillable Reporting Template”. The aim of the U.S. EPA report was an attempt to standardize the way sensor performance is reported. To that end, this part of the tutorial builds all of the relevant plots and tables required in the Fillable Report. We will use our three Burnaby South PurpleAir monitors for this purpose.

This analysis is not extensive; there are numerous details and caveats mentioned within the full U.S. EPA report (Duvall et al. 2021). It is strongly recommended that in addition to this tutorial, any low-cost PM sensor users read the U.S. EPA report in full. Additionally, this tutorial is only applicable to “Base Testing” (i.e., field tests). The U.S. EPA also recommends “Enhanced Testing” based on lab-based analyses. This is not covered here.

This tutorial covers the following elements of the Fillable Report for Base Testing:

  • Time Series Plots
  • Scatter Plots: Comparison to FRM/FEM
  • Performance Metrics
    • Sensor-FRM/FEM Accuracy
    • Sensor-Sensor Precision
  • Meteorological Conditions During Deployment
  • Meteorological Influence
  • Individual Sensor-FRM/FEM Scatter Plots
  • Tabular Statistics

Time Series Plots

Here we plot time series for all three PurpleAir monitors and the FRM/FEM monitor for both 1 h and 24 h average PM2.5 concentrations.

ggplot(df_1h,aes(x=datetime_1h,y=data_1h,group=Location,color=Location)) +
  geom_line(alpha=0.7,size=1) +
  xlab("Date") +
  ylab(expression(PM[2.5]~Concentration~(µg/m^3))) +
  ylim(0,200) +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20)) +
  ggtitle("1 h Average")

ggplot(df_24h,aes(x=datetime_24h,y=data_24h,group=Location,color=Location)) +
  geom_line(alpha=0.7,size=1) + 
  xlab("Date") +
  ylab(expression(PM[2.5]~Concentration~(µg/m^3))) +
  ylim(0,200) +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20)) +
  ggtitle("24 h Average")

From the time series plots, we clearly see a period where the PurpleAir sensors are overestimating PM2.5 concentrations (early October). The scale on the y-axis is also influenced by a wildfire smoke episode which occurred in mid-September 2020.

Scatter Plots: Comparison to FRM/FEM

Here we scatter plots of all three PurpleAir monitors vs. the FRM/FEM monitor for both 1 h and 24 h average PM2.5 concentrations. There is also a 1:1 line added to quickly assess whether the PurpleAir monitors are over- or under-estimating.

df_1h_scatter <- spread(df_1h,Location,data_1h)
df_1h_scatter <- gather(df_1h_scatter,Location,data_1h,-datetime_1h,-FEM)

ggplot(df_1h_scatter,aes(x=FEM,y=data_1h,group=Location,color=Location)) +
  geom_point(alpha=0.7,size=1) +
  xlab(expression(FEM~PM[2.5]~(µg/m^3))) +
  ylab(expression(Sensor~PM[2.5]~(µg/m^3))) +
  ggtitle("1 h Average") +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20)) +
  xlim(0,30) +
  ylim(0,30) +
  geom_abline(slope=1,linetype="dashed") +
  coord_equal()

df_24h_scatter <- spread(df_24h,Location,data_24h)
df_24h_scatter <- gather(df_24h_scatter,Location,data_24h,-datetime_24h,-FEM)

ggplot(df_24h_scatter,aes(x=FEM,y=data_24h,group=Location,color=Location)) +
  geom_point(alpha=0.7,size=2) +
  xlab(expression(FEM~PM[2.5]~(µg/m^3))) +
  ylab(expression(Sensor~PM[2.5]~(µg/m^3))) +
  ggtitle("24 h Average") +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20)) +
  xlim(0,30) +
  ylim(0,30) +
  geom_abline(slope=1,linetype="dashed") +
  coord_equal()

From these scatter plots zoomed in to 30 μg/m3 , we can see that the PurpleAir monitors tend to slightly overestimate concentrations (data falls above 1:1 line). We will explicitly calculate the slope in the “Performance Metrics” section.

Performance Metrics

The U.S. EPA PM2.5 Testing Report recommends reporting on the following metrics:

  • Sensor Accuracy Metrics
    • Coefficient of Determination (R2)
    • Slope
    • Intercept
    • Root Mean Square Error (RMSE)
    • Normalized Root Mean Square Error (NRMSE)
  • Sensor Precision Metrics
    • Standard Deviation (SD)
    • Coefficient of Variation (CV)

These are first defined and then calculated in the respective sub-sections. For all equations in this section we will assume that x is the FEM/FRM data and y is the sensor data.

Coefficient of Determination (R2)

The coefficient of determination is a metric that assess how differences in one variable can be explained by the difference in a second variable.

\[R^2 = 1-\frac{\sum(y_i -x_i)^2}{\sum(y_i-\bar{y})^2}\] Slope (m)

Slope is calculated by finding the ratio of the “vertical change” (rise) to the “horizontal change” (run) between (any) two distinct points on a line. For a simple linear regression between the FEM/FRM data (x) and the sensor data (y), it is calculated as:

\[m=\frac{\sum(y_i-\bar{y})\times(x_i-\bar{x})}{\sum(x_i-\bar{x})^2}\] Intercept (b)

This is the value at which the fitted line from a simple linear regression crosses the y-axis. For a simple linear regression between the FEM/FRM data (x) and the sensor data (y), it is calculated as:

\[ b = \bar{y} - m\times\bar{x}\]

Root Mean Square Error (RMSE)

The RMSE is measure of the differences between the sensor data (y) and the FEM/FRM monitor data (x).

\[RMSE=\sqrt{\frac{1}{N}\sum_{i=1}^{N}(y_i -x_i)^2}\]

If you wish to calculate one RMSE across multiple (M) sensors, the following adjusted formula is used:

\[RMSE=\sqrt{\frac{1}{N\times M}\sum_{j=1}^{M}\sum_{i=1}^{N}(y_{ij} -x_i)^2}\] Normalized Root Mean Square Error (NRMSE)

The NRMSE helps account for periods when ambient concentrations are high (which may skew the RMSE). It is calculated by normalizing the RMSE by the average concentration measured by the FEM/FRM monitor.

\[NRMSE = \frac{RMSE}{\bar{x}}\times100\]

Standard Deviation

This is the standard deviation of the sensor PM_2.5_ concentration measurements. It is calculated using data from all co-deployed low-cost sensors. It is defined as follows:

\[SD=\sqrt{\frac{1}{(N\times M)-1}\sum_{j=1}^{M}\sum_{d=1}^{N}(y_{dj} -\bar{y_d})^2}\]

Where M is the number of identical sensors operated simultaneously, N is the number of periods during which all instruments are operating and reporting valid data, ydj is the average concentration for day or hour d and sensor j, and \(\bar{y_d}\) is the average concentration across all sensors for day or hour d.

Coefficient of Variation (CV)

This is the relative standard deviation (SD) divided by the deployment averaged sensor PM2.5 concentration across the field test. It is defined as follows:

\[CV= \frac{SD}{\bar{y}}\times 100\] Where \(\bar{y}\) is the deployment averaged sensor PM_2.5_ concentration for the field test.

Sensor-FRM/FEM Accuracy

We will calculate accuracy metrics for both hourly and daily averaging periods. We will also provide tabular statistics, as recommended. We will use built-in linear regression functions in R to calculate slope, intercept, and R2. RMSE and NRMSE are calculated manually. At the end of this code chunk the data are reported in tabular format, followed by a section plotting the results.

## 1 hour data

# Add a column to our data frame with the square residual. This is helpful for RMSE calculations.

df_1h_accuracy <- df_1h_scatter %>%
  mutate(sq.residual=(data_1h - FEM)^2)

# Slope, intercept and R2. Quickly calculated using R Package 'brooms' and their tidy and glance functions

model_params_1h <- df_1h_accuracy %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_1h ~ FEM, data = .)), tidied = map(model, tidy)) %>% unnest(tidied) 

model_stats_1h <- df_1h_accuracy %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_1h ~ FEM, data = .)), tidied = map(model, glance)) %>% unnest(tidied)

slopes_1h <- model_params_1h %>% filter(term=="FEM") %>% select(Location,estimate) %>% rename(.,slope_1h=estimate)
intercepts_1h <- model_params_1h %>% filter(term=="(Intercept)") %>% select(Location,estimate) %>% rename(.,intercept_1h=estimate)
r2_1h <- model_stats_1h %>% select(Location,r.squared) %>% rename(.,r2_1h=r.squared)

# RMSE and NRMSE: Manual calculations. 

# Calculate RMSE and NRMSE
RMSE_1h <- df_1h_accuracy %>%
  group_by(Location) %>%
  summarize(RMSE_1h=sqrt(mean(sq.residual,na.rm=T)),
            NRMSE_1h=sqrt(mean(sq.residual,na.rm=T))/mean(FEM,na.rm=T)*100)

accuracy_1h <-  left_join(r2_1h, slopes_1h, by='Location') %>%
                left_join(., intercepts_1h, by='Location') %>%
                left_join(.,RMSE_1h,by='Location')

## 24 hour data

# Add a column to our data frame with the square residual. Helpful for RMSE calculations.
df_24h_accuracy <- df_24h_scatter %>%
  mutate(sq.residual=(data_24h - FEM)^2)

# Slope, intercept and R2. Quickly calculated using R Package 'brooms' and their tidy and glance functions

model_params_24h <- df_24h_accuracy %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_24h ~ FEM, data = .)), tidied = map(model, tidy)) %>% unnest(tidied) 

model_stats_24h <- df_24h_accuracy %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_24h ~ FEM, data = .)), tidied = map(model, glance)) %>% unnest(tidied)

slopes_24h <- model_params_24h %>% filter(term=="FEM") %>% select(Location,estimate) %>% rename(.,slope_24h=estimate)
intercepts_24h <- model_params_24h %>% filter(term=="(Intercept)") %>% select(Location,estimate) %>% rename(.,intercept_24h=estimate)
r2_24h <- model_stats_24h %>% select(Location,r.squared) %>% rename(.,r2_24h=r.squared)

# RMSE and NRMSE: : Manual calculations

RMSE_24h <- df_24h_accuracy %>%
  group_by(Location) %>%
  summarize(RMSE_24h=sqrt(mean(sq.residual,na.rm=T)),
            NRMSE_24h=sqrt(mean(sq.residual,na.rm=T))/mean(FEM, na.rm=T)*100)

accuracy_24h <-  left_join(r2_24h, slopes_24h, by='Location') %>%
                left_join(., intercepts_24h, by='Location') %>%
                left_join(.,RMSE_24h,by='Location')

accuracy <- left_join(accuracy_1h,accuracy_24h,by='Location')

# We will use this grouped data later when plotting
accuracy_long <- melt(setDT(accuracy),id.vars='Location',measure.vars=patterns("^r2_","^slope_","^intercept_","^RMSE_","^NRMSE_"),value.name = c("r2", "slope","intercept","RMSE","NRMSE"),variable.name = "avg.time")
accuracy_long$avg.time <- recode_factor(accuracy_long$avg.time,`1`="1 h",`2`="24 h")


## Print metrics as table

accuracy_mean <- accuracy %>% summarize_if(is.numeric,mean)
accuracy <- bind_rows(accuracy,accuracy_mean)
accuracy[4,1] = "Mean"

accuracy %>%
  kbl(caption="**Sensor-FEM Accuracy Metrics**", digits=2, col.names = c("Sensor ID","R^2^","Slope","Intercept","RMSE","NRMSE","R^2^","Slope","Intercept","RMSE","NRMSE")) %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  add_header_above(c(" " = 1, "1 h" = 5, "24 h" = 5))
Sensor-FEM Accuracy Metrics
1 h
24 h
Sensor ID R2 Slope Intercept RMSE NRMSE R2 Slope Intercept RMSE NRMSE
Burnaby1 0.91 1.00 3.43 8.77 86.03 0.94 0.96 3.66 7.64 67.29
Burnaby2 0.71 1.12 2.45 18.55 181.98 0.91 1.06 2.80 10.13 89.28
Burnaby3 0.90 0.98 3.59 8.72 85.54 0.93 0.93 3.83 7.65 67.42
Mean 0.84 1.03 3.16 12.02 117.85 0.93 0.98 3.43 8.47 74.66

Now we will plot the results using box plots.

# Prep data for box plots

ggplot(accuracy_long,aes(x=avg.time,y=r2,fill=avg.time)) +
  geom_boxplot(show.legend = F) +
  geom_dotplot(binaxis='y', stackdir='center', dotsize=1, show.legend=F) +
  xlab("Averaging Time") +
  ylab(expression(R^2)) +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

ggplot(accuracy_long,aes(x=avg.time,y=slope,fill=avg.time)) +
  geom_boxplot(show.legend = F) +
  geom_dotplot(binaxis='y', stackdir='center', dotsize=1, show.legend=F) +
  xlab("Averaging Time") +
  ylab("Slope") +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

ggplot(accuracy_long,aes(x=avg.time,y=intercept,fill=avg.time)) +
  geom_boxplot(show.legend = F) +
  geom_dotplot(binaxis='y', stackdir='center', dotsize=1, show.legend=F) +
  xlab("Averaging Time") +
  ylab(expression(Intercept~(μg/m^3))) +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

ggplot(accuracy_long,aes(x=avg.time,y=RMSE,fill=avg.time)) +
  geom_boxplot(show.legend = F) +
  geom_dotplot(binaxis='y', stackdir='center', dotsize=1, show.legend=F) +
  xlab("Averaging Time") +
  ylab(expression(RMSE~(μg/m^3))) +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

ggplot(accuracy_long,aes(x=avg.time,y=NRMSE,fill=avg.time)) +
  geom_boxplot(show.legend = F) +
  geom_dotplot(binaxis='y', stackdir='center', dotsize=1, show.legend=F) +
  xlab("Averaging Time") +
  ylab("NRMSE (%)") + 
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

Sensor-Sensor Precision

We will calculate sensor-sensor precision metrics for both hourly and daily averaging periods. We will also provide tabular statistics, as recommended. SD and CV are calculated manually. At the end of this code chunk the data are reported in tabular format. Since only one value is reported for SD and CV (%), there are no plots, only tabular data.

## 1 hour

df_1h_precision <- spread(df_1h,Location,data_1h) 

# Calculate the average across all three PurpleAir sensors using rowMeans. Save as new column "avg_1h"
df_1h_precision <- df_1h_precision %>%
  mutate(., avg_1h = rowMeans(select(., starts_with("Burnaby")), na.rm = TRUE)) 

# Calculate the square residual of each sensor vs. the mean across sensors
df_1h_precision <- df_1h_precision %>%
  mutate(.,sq.resid.Burnaby1 = (Burnaby1-avg_1h)^2,sq.resid.Burnaby2 = (Burnaby2-avg_1h)^2,sq.resid.Burnaby3 = (Burnaby3-avg_1h)^2) 
 
# Calculate the sum of the square residual for each sensor
sum.sq.resid_1h  <- df_1h_precision %>%
  summarize(sum.sq.resid.Burnaby1 = sum(sq.resid.Burnaby1),
            sum.sq.resid.Burnaby2 = sum(sq.resid.Burnaby2),
            sum.sq.resid.Burnaby3 = sum(sq.resid.Burnaby3))

# Compute SD and CV (%)
SD_1h = sqrt(sum(sum.sq.resid_1h)/((3*nrow(df_1h_precision))-1))
avg_1h_all <- mean(df_1h_precision$avg_1h)
CV_1h <- SD_1h / avg_1h_all * 100

## 24 hour
df_24h_precision <- spread(df_24h,Location,data_24h) 

# Calculate the average across all three PurpleAir sensors using rowMeans. Save as new column "avg_24h"
df_24h_precision <- df_24h_precision %>%
  mutate(., avg_24h = rowMeans(select(., starts_with("Burnaby")), na.rm = TRUE)) 

# Calculate the square residual of each sensor vs. the mean across sensors
df_24h_precision <- df_24h_precision %>%
  mutate(.,sq.resid.Burnaby1 = (Burnaby1-avg_24h)^2,sq.resid.Burnaby2 = (Burnaby2-avg_24h)^2,sq.resid.Burnaby3 = (Burnaby3-avg_24h)^2) 

# Calculate the sum of the square residual for each sensor 
sum.sq.resid_24h  <- df_24h_precision %>%
  summarize(sum.sq.resid.Burnaby1 = sum(sq.resid.Burnaby1),
            sum.sq.resid.Burnaby2 = sum(sq.resid.Burnaby2),
            sum.sq.resid.Burnaby3 = sum(sq.resid.Burnaby3))

# Compute SD and CV (%)
SD_24h = sqrt(sum(sum.sq.resid_24h)/((3*nrow(df_24h_precision))-1))
avg_24h_all <- mean(df_24h_precision$avg_24h)
CV_24h <- SD_24h / avg_24h_all * 100

## Print metrics as table

precision <- as.tibble(cbind(CV_1h,SD_1h,CV_24h,SD_24h))

precision %>%
  kbl(caption="**Sensor Precision Metrics**", digits=2, col.names = c("SD (μg/m^3^)","CV (%)","SD (μg/m^3^)","CV (%)")) %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  add_header_above(c("1 h" = 2, "24 h" = 2))
Sensor Precision Metrics
1 h
24 h
SD (µg/m3) CV (%) SD (µg/m3) CV (%)
51.67 7.59 18.42 2.69

Sensor-FEM Accuracy: Separating High Concentration Events

Some of our performance metrics may be skewed by periods with very high concentrations (e.g., wildfires). We will also calculate accuracy performance metrics separately during high concentration episodes and lower concentration periods. Here we will look at performance when splitting based on whether concentrations exceed 25 µg/m3. The results will be reported in tabular format.

This is coded very similarly to the previous “Performance Metrics” subsection “Sensor-FEM Accuracy,” with the caveat that dplyr “filters” were applied to split the data by the reported FEM concentration.

## 1 hour data

# Slope, intercept and R2 split by high and low using "filter". Quickly calculated using R Package 'brooms' and their tidy and glance functions

model_params_1h_high <- df_1h_accuracy %>% filter(FEM >= 25) %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_1h ~ FEM, data = .)), tidied = map(model, tidy)) %>% unnest(tidied) 

model_params_1h_low <- df_1h_accuracy %>% filter(FEM < 25) %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_1h ~ FEM, data = .)), tidied = map(model, tidy)) %>% unnest(tidied) 

model_stats_1h_high <- df_1h_accuracy %>% filter(FEM >= 25) %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_1h ~ FEM, data = .)), tidied = map(model, glance)) %>% unnest(tidied)

model_stats_1h_low <- df_1h_accuracy %>% filter(FEM < 25) %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_1h ~ FEM, data = .)), tidied = map(model, glance)) %>% unnest(tidied)

slopes_1h_high <- model_params_1h_high %>% filter(term=="FEM") %>% select(Location,estimate) %>% rename(.,slope_1h=estimate)
intercepts_1h_high <- model_params_1h_high %>% filter(term=="(Intercept)") %>% select(Location,estimate) %>% rename(.,intercept_1h=estimate)
r2_1h_high <- model_stats_1h_high %>% select(Location,r.squared) %>% rename(.,r2_1h=r.squared)

slopes_1h_low <- model_params_1h_low %>% filter(term=="FEM") %>% select(Location,estimate) %>% rename(.,slope_1h=estimate)
intercepts_1h_low <- model_params_1h_low %>% filter(term=="(Intercept)") %>% select(Location,estimate) %>% rename(.,intercept_1h=estimate)
r2_1h_low <- model_stats_1h_low %>% select(Location,r.squared) %>% rename(.,r2_1h=r.squared)

# RMSE and NRMSE: Manual calculations
avg_1h_high <-df_1h_accuracy %>%
  filter(FEM >= 25) %>%
  summarize(mean(FEM,na.rm=T))


RMSE_1h_high <- df_1h_accuracy %>%
  filter(FEM >= 25) %>%
  group_by(Location) %>%
  summarize(RMSE_1h=sqrt(mean(sq.residual,na.rm=T)),
            NRMSE_1h=sqrt(mean(sq.residual,na.rm=T))/mean(FEM,na.rm=T)*100)

RMSE_1h_low <- df_1h_accuracy %>%
  filter(FEM < 25) %>%
  group_by(Location) %>%
  summarize(RMSE_1h=sqrt(mean(sq.residual,na.rm=T)),
            NRMSE_1h=sqrt(mean(sq.residual,na.rm=T))/mean(FEM,na.rm=T)*100)

accuracy_1h_low <-  left_join(r2_1h_low, slopes_1h_low, by='Location') %>%
                left_join(., intercepts_1h_low, by='Location') %>%
                left_join(.,RMSE_1h_low,by='Location')

accuracy_1h_high <-  left_join(r2_1h_high, slopes_1h_high, by='Location') %>%
                left_join(., intercepts_1h_high, by='Location') %>%
                left_join(.,RMSE_1h_high,by='Location')

## 24 hour data

# Slope, intercept and R2 split by high and low using "filter". Quickly calculated using R Package 'brooms' and their tidy and glance functions

model_params_24h_high <- df_24h_accuracy %>% filter(FEM >= 25) %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_24h ~ FEM, data = .)), tidied = map(model, tidy)) %>% unnest(tidied) 

model_params_24h_low <- df_24h_accuracy %>% filter(FEM < 25) %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_24h ~ FEM, data = .)), tidied = map(model, tidy)) %>% unnest(tidied) 

model_stats_24h_high <- df_24h_accuracy %>% filter(FEM >= 25) %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_24h ~ FEM, data = .)), tidied = map(model, glance)) %>% unnest(tidied)

model_stats_24h_low <- df_24h_accuracy %>% filter(FEM < 25) %>% nest(data = -Location) %>% 
  mutate(model = map(data, ~lm(data_24h ~ FEM, data = .)), tidied = map(model, glance)) %>% unnest(tidied)

slopes_24h_high <- model_params_24h_high %>% filter(term=="FEM") %>% select(Location,estimate) %>% rename(.,slope_24h=estimate)
intercepts_24h_high <- model_params_24h_high %>% filter(term=="(Intercept)") %>% select(Location,estimate) %>% rename(.,intercept_24h=estimate)
r2_24h_high <- model_stats_24h_high %>% select(Location,r.squared) %>% rename(.,r2_24h=r.squared)

slopes_24h_low <- model_params_24h_low %>% filter(term=="FEM") %>% select(Location,estimate) %>% rename(.,slope_24h=estimate)
intercepts_24h_low <- model_params_24h_low %>% filter(term=="(Intercept)") %>% select(Location,estimate) %>% rename(.,intercept_24h=estimate)
r2_24h_low <- model_stats_24h_low %>% select(Location,r.squared) %>% rename(.,r2_24h=r.squared)

# RMSE and NRMSE: Manual calculations

RMSE_24h_high <- df_24h_accuracy %>%
  filter(FEM >= 25) %>%
  group_by(Location) %>%
  summarize(RMSE_24h=sqrt(mean(sq.residual,na.rm=T)),
            NRMSE_24h=sqrt(mean(sq.residual,na.rm=T))/mean(FEM,na.rm=T)*100)

RMSE_24h_low <- df_24h_accuracy %>%
  filter(FEM < 25) %>%
  group_by(Location) %>%
  summarize(RMSE_24h=sqrt(mean(sq.residual,na.rm=T)),
            NRMSE_24h=sqrt(mean(sq.residual,na.rm=T))/mean(FEM,na.rm=T)*100)

accuracy_24h_low <-  left_join(r2_24h_low, slopes_24h_low, by='Location') %>%
                left_join(., intercepts_24h_low, by='Location') %>%
                left_join(.,RMSE_24h_low,by='Location')

accuracy_24h_high <-  left_join(r2_24h_high, slopes_24h_high, by='Location') %>%
                left_join(., intercepts_24h_high, by='Location') %>%
                left_join(.,RMSE_24h_high,by='Location')

#####

accuracy_low <- left_join(accuracy_1h_low,accuracy_24h_low,by='Location')
accuracy_high <- left_join(accuracy_1h_high,accuracy_24h_high,by='Location')

# We will use this grouped data later when plotting
accuracy_long_high <- melt(setDT(accuracy_high),id.vars='Location',measure.vars=patterns("^r2_","^slope_","^intercept_","^RMSE_","^NRMSE_"),value.name = c("r2", "slope","intercept","RMSE","NRMSE"),variable.name = "avg.time")
accuracy_long_high$avg.time <- recode_factor(accuracy_long_high$avg.time,`1`="1 h",`2`="24 h")

accuracy_long_low <- melt(setDT(accuracy_high),id.vars='Location',measure.vars=patterns("^r2_","^slope_","^intercept_","^RMSE_","^NRMSE_"),value.name = c("r2", "slope","intercept","RMSE","NRMSE"),variable.name = "avg.time")
accuracy_long_low$avg.time <- recode_factor(accuracy_long_low$avg.time,`1`="1 h",`2`="24 h")


## Print metrics as table

accuracy_mean_high <- accuracy_high %>% summarize_if(is.numeric,mean)
accuracy_high <- bind_rows(accuracy_high,accuracy_mean_high)
accuracy_high[4,1] = "Mean"

accuracy_high %>%
  kbl(caption="**Sensor-FEM Accuracy Metrics, >=25 μg/m^3^**", digits=2, col.names = c("Sensor ID","R^2^","Slope","Intercept","RMSE","NRMSE","R^2^","Slope","Intercept","RMSE","NRMSE")) %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  add_header_above(c(" " = 1, "1 h" = 5, "24 h" = 5))
Sensor-FEM Accuracy Metrics, >=25 µg/m3
1 h
24 h
Sensor ID R2 Slope Intercept RMSE NRMSE R2 Slope Intercept RMSE NRMSE
Burnaby1 0.81 0.77 29.92 23.65 25.22 0.86 0.76 27.38 18.87 18.22
Burnaby2 0.24 0.72 49.74 70.20 74.87 0.66 0.64 53.85 32.58 31.46
Burnaby3 0.79 0.74 30.82 23.77 25.34 0.85 0.73 28.25 19.67 19.00
Mean 0.61 0.75 36.83 39.20 41.81 0.79 0.71 36.50 23.71 22.89
accuracy_mean_low <- accuracy_low %>% summarize_if(is.numeric,mean)
accuracy_low <- bind_rows(accuracy_low,accuracy_mean_low)
accuracy_low[4,1] = "Mean"

accuracy_low %>%
  kbl(caption="**Sensor-FEM Accuracy Metrics, <25 μg/m^3^**", digits=2, col.names = c("Sensor ID","R^2^","Slope","Intercept","RMSE","NRMSE","R^2^","Slope","Intercept","RMSE","NRMSE")) %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  add_header_above(c(" " = 1, "1 h" = 5, "24 h" = 5))
Sensor-FEM Accuracy Metrics, <25 µg/m3
1 h
24 h
Sensor ID R2 Slope Intercept RMSE NRMSE R2 Slope Intercept RMSE NRMSE
Burnaby1 0.56 1.57 0.44 6.72 143.22 0.69 1.92 -1.26 6.12 125.43
Burnaby2 0.56 1.59 -0.39 6.51 138.80 0.68 1.94 -2.09 5.95 121.92
Burnaby3 0.56 1.55 0.59 6.62 141.15 0.69 1.86 -0.90 5.96 122.13
Mean 0.56 1.57 0.21 6.62 141.06 0.69 1.91 -1.42 6.01 123.16

In this scenario, the split analysis provides useful insight: the PurpleAir monitors are tending to overestimate at low concentrations (slopes > 1) and underestimate during the wildfire period (slopes < 1). This suggests we might also need piece-wise calibrations for low and high concentrations, such as what is discussed in the literature (Malings et al. 2020). We will demonstrate this in Part 3. We also see that the sensors seem to be more accurate at high concentration; NRMSE is generally below 30% for high concentration data.

Meteorological Conditions During Deployment

We will need to download some meteorological station data to report on the meteorological conditions during the deployment. It is important to note that the internal temperature and relative humidity sensors inside low-cost sensor units are often influenced by their internal environment. In general it is strongly preferred to report externally monitored weather data.

To get regulatory weather station data in Canada, we will use the “weathercan” package (LaZerte 2021). If you want other meteorological data for other parts of the world, this section of code would need to be adapted, but that is outside the scope of this tutorial. Consider checking your local regulatory monitoring network or using R packages designed to import weather from elsewhere (e.g., “rnoaa” for NOAA data).

In this section, we download the 1 h weather data, convert it to 24 h averages using the “timeAverage” function in the R Package “openair” (Carslaw and Ropkins 2020), and plot histograms.

closest_station <- stations_search(coords=c(49.2162,-122.9852), interval="hour", ends_earliest = "2021")

weather_1h <- weather_dl(station_ids = closest_station$station_id,start="2020-09-01",end="2020-12-31",interval="hour")
weather_1h <- weather_1h %>% select(time,rel_hum,temp,temp_dew) %>% rename(.,date=time)

weather_24h <- timeAverage(weather_1h,avg.time="day")

ggplot(weather_24h, aes(x = temp)) + 
  geom_histogram(aes(y = stat(count) / sum(count)), bins = 8,colour="black",fill="darkgoldenrod1") +
  scale_y_continuous(labels = scales::percent) +
  xlab("Temperature (°C)") +
  ylab("Relative Probability") +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

ggplot(weather_24h, aes(x = rel_hum)) + 
  geom_histogram(aes(y = stat(count) / sum(count)), bins = 12,colour="black",fill="darkolivegreen3") +
  scale_y_continuous(labels = scales::percent) +
  xlab("Relative Humidity (%)") +
  ylab("Relative Probability") +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

From these plots note that a lot of our data is at high relative humidity - it rains a lot in Vancouver! High relative humidity is known to result in overestimated low-cost PM2.5 sensor readings (Malings et al. 2020).

Meteorological Influence

In this section, we plot the normalized PM2.5 concentration (defined as Sensor PM2.5 / FEM PM2.5) for each 24 hour period against the monitored temperature and relative humidity. We will also show scatter plots of FEM vs Sensor PM2.5 as coloured by relative humidity. To do this, we must first join our weather data to our sensor data and then calculate normalized concentrations. When we are finished, we will plot the results.

## Prep the data - Add weather data and calculate normalized concentrations

weather_24h <- rename(weather_24h,datetime_24h = date)
df_24h_all <- full_join(df_24h_precision,weather_24h,by="datetime_24h") %>% select(-avg_24h,-sq.resid.Burnaby1,-sq.resid.Burnaby2,-sq.resid.Burnaby3)

# Add new columns for normalized PM2.5 data
df_24h_all <- df_24h_all %>%
  mutate(.,norm_Burnaby1=Burnaby1/FEM,norm_Burnaby2=Burnaby2/FEM,norm_Burnaby3 = Burnaby3/FEM)

# Let's pivot this into a convenient format for plotting
df_24h_all_long <- df_24h_all %>% 
  pivot_longer(cols=starts_with("norm_"),names_to="Location",names_prefix="norm_",values_to="norm_conc")

## Plot the data

ggplot(df_24h_all_long,aes(x=temp,y=norm_conc,group=Location,color=Location)) +
  geom_point(alpha=0.7,size=2) +
  ylim(-1,5) +
  xlab("Temperature (°C)") +
  ylab("Normalized Concentration") +
  geom_hline(yintercept=1,linetype="dashed") +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

ggplot(df_24h_all_long,aes(x=rel_hum,y=norm_conc,group=Location,color=Location)) +
  geom_point(alpha=0.7,size=2) +
  ylim(-1,5) +
  xlab("Relative Humidity (%)") +
  ylab("Normalized Concentration") +
  geom_hline(yintercept=1,linetype="dashed") +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))


## Individual Sensor-FEM scatter plots coloured by relative humidity

ggplot(df_24h_all,aes(x=FEM,y=Burnaby1,colour=rel_hum)) +
  geom_point(size=2) +
  scale_colour_gradient2(low="blue", mid="white", high="red", midpoint=50,limits=c(0,100)) +
  labs(title="Burnaby 1", x=expression(FEM/FRM~PM[2.5]~Concentration~(μg/m^3)), y=expression(Sensor~PM[2.5]~Concentration~(μg/m^3)), color="RH (%)") +
  geom_abline(slope=1,linetype="dashed") +
  xlim(0,30) +
  ylim(0,30) +
  coord_equal() +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

ggplot(df_24h_all,aes(x=FEM,y=Burnaby2,colour=rel_hum)) +
  geom_point(size=2) +
  scale_colour_gradient2(low="blue", mid="white", high="red", midpoint=50,limits=c(0,100)) +
  labs(title="Burnaby 2", x=expression(FEM/FRM~PM[2.5]~Concentration~(μg/m^3)), y=expression(Sensor~PM[2.5]~Concentration~(μg/m^3)), color="RH (%)") +
  geom_abline(slope=1,linetype="dashed") +
  xlim(0,30) +
  ylim(0,30) +
  coord_equal() +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

ggplot(df_24h_all,aes(x=FEM,y=Burnaby3,colour=rel_hum)) +
  geom_point(size=2) +
  scale_colour_gradient2(low="blue", mid="white", high="red", midpoint=50,limits=c(0,100)) +
  labs(title="Burnaby 3", x=expression(FEM/FRM~PM[2.5]~Concentration~(μg/m^3)), y=expression(Sensor~PM[2.5]~Concentration~(μg/m^3)), color="RH (%)") +
  geom_abline(slope=1,linetype="dashed") +
  xlim(0,30) +
  ylim(0,30) +
  coord_equal() +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=20))

Tabular Statistics

In addition to the tabular data already provided within the “Performance Metrics” section, it is also recommended to provide tabular data on Uptime (%) and number of paired observations in the analysis. Specifically, it is recommended that you report the following:

  1. Range of FRM/FEM monitor concentrations over duration of test (μg/m3)
  2. Number of 24-hour periods in FRM/FEM monitor measurements with a goal concentration of ≥ 25 μg/m3
  3. Number of 24-hour periods outside manufacturer-listed temperature target criteria
  4. Number of 24-hour periods outside manufacturer-listed relative humidity target criteria
  5. Number of concurrently reported sensor concentration values (1 h and 24 h)
  6. Number of paired sensor and FRM/FEM concentration values (1 h and 24 h)
  7. Number of paired 24-hour, normalized concentration and temperature values
  8. Number of paired 24-hour, normalized concentration and relative humidity values
  9. Sensor uptime (%) (1 h and 24 h)

Many of these calculations are simple to do with the “complete.cases” function in R.

Note that for items 3-4 (# of periods outside manufacturer-listed temperature and relative humidity target criteris) for PurpleAir monitors, according to the manufacturer, each sensor will work effectively in a temperature range of -10-60°C and RH range of 0–99 % (Yong 2016).

Let’s start with the calculations. There are many lines of code here, but most are simple steps.

## 1. Range of FRM/FEM monitor concentrations over duration of test (μg/m^3^)
df_1h %>%
  group_by(Location) %>%
  summarize(Min=min(data_1h,na.rm=T),
            Max=max(data_1h,na.rm=T)) %>%
  kbl(caption="**PM~2.5~ Concentration Range**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
PM2.5 Concentration Range
Location Min Max
Burnaby1 0 171.8502
Burnaby2 0 724.4550
Burnaby3 0 165.1532
FEM 0 243.1000
## 2. Number of 24-hour periods in FRM/FEM monitor measurements with a goal concentration of ≥ 25 μg/m^3^
df_24h %>%
  filter(data_24h > 25) %>%
    count(Location) %>%
  kbl(caption="**# 24 h Average Data Points Above 25 µg/m^3^**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
# 24 h Average Data Points Above 25 µg/m3
Location n
Burnaby1 15
Burnaby2 14
Burnaby3 14
FEM 8
## 3. Number of 24-hour periods outside manufacturer-listed temperature target criteria

# Here we use 'cut' to add breaks to the data based on our known guidelines (-10 to 60)

df_24h_all %>% mutate(
  range = cut(temp, breaks = c(-Inf,-10, 60, Inf))) %>%
  group_by(range) %>%
  summarize(count = n()) %>%
  kbl(caption="**Range of 24 h T Readings (none outside of range)**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Range of 24 h T Readings (none outside of range)
range count
(-10,60] 122
## 4. Number of 24-hour periods outside manufacturer-listed relative humidity target criteria

# Here we use 'cut' to add breaks to the data based on our known guidelines (0 to 99)

df_24h_all %>% mutate(
  range = cut(rel_hum, breaks = c(0, 99, Inf))) %>%
  group_by(range) %>%
  summarize(count = n()) %>%
  kbl(caption="**Range of 24 h RH Readings (none outside of range)**") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Range of 24 h RH Readings (none outside of range)
range count
(0,99] 122
## 5. Number of concurrently reported sensor concentration values (1 h and 24 h) &
## 6. Number of paired sensor and FRM/FEM concentration values (1 h and 24 h)

# We will tabulate items 5-6 simultaneously. Note this section of code is a little clunky - apologies!

# Get paired number of observations for 1 h and 24 h averages using "complete.cases"
paired_1h_B1.FEM <- sum(complete.cases(data_1h_Burnaby1,data_1h_FEM))
paired_1h_B2.FEM <- sum(complete.cases(data_1h_Burnaby2,data_1h_FEM))
paired_1h_B3.FEM <- sum(complete.cases(data_1h_Burnaby3,data_1h_FEM))

paired_1h_B1.B2 <- sum(complete.cases(data_1h_Burnaby1,data_1h_Burnaby2))
paired_1h_B1.B3 <- sum(complete.cases(data_1h_Burnaby1,data_1h_Burnaby3))
paired_1h_B2.B3 <- sum(complete.cases(data_1h_Burnaby2,data_1h_Burnaby3))

paired_24h_B1.FEM <- sum(complete.cases(data_24h_Burnaby1,data_24h_FEM))
paired_24h_B2.FEM <- sum(complete.cases(data_24h_Burnaby2,data_24h_FEM))
paired_24h_B3.FEM <- sum(complete.cases(data_24h_Burnaby3,data_24h_FEM))

paired_24h_B1.B2 <- sum(complete.cases(data_24h_Burnaby1,data_24h_Burnaby2))
paired_24h_B1.B3 <- sum(complete.cases(data_24h_Burnaby1,data_24h_Burnaby3))
paired_24h_B2.B3 <- sum(complete.cases(data_24h_Burnaby2,data_24h_Burnaby3))

# Store data in a new data frame for reporting
pairs_names <- c("FEM","Burnaby1","Burnaby2","Burnaby3")
pairs_1h_FEM <- c(NA,paired_1h_B1.FEM,paired_1h_B2.FEM,paired_1h_B3.FEM)
pairs_1h_Burnaby1 <- c(paired_1h_B1.FEM,NA,paired_1h_B1.B2,paired_1h_B1.B3)
pairs_1h_Burnaby2 <- c(paired_1h_B2.FEM,paired_1h_B1.B2,NA,paired_1h_B2.B3)
pairs_1h_Burnaby3 <- c(paired_1h_B3.FEM,paired_1h_B1.B3,paired_1h_B2.B3,NA)
pairs_1h <- data.frame(pairs_names,pairs_1h_FEM,pairs_1h_Burnaby1,pairs_1h_Burnaby2,pairs_1h_Burnaby3)
names(pairs_1h) <- c("Monitor","FEM.1h","Burnaby1.1h","Burnaby2.1h","Burnaby3.1h")

pairs_24h_FEM <- c(NA,paired_24h_B1.FEM,paired_24h_B2.FEM,paired_24h_B3.FEM)
pairs_24h_Burnaby1 <- c(paired_24h_B1.FEM,NA,paired_24h_B1.B2,paired_24h_B1.B3)
pairs_24h_Burnaby2 <- c(paired_24h_B2.FEM,paired_24h_B1.B2,NA,paired_24h_B2.B3)
pairs_24h_Burnaby3 <- c(paired_24h_B3.FEM,paired_24h_B1.B3,paired_24h_B2.B3,NA)
pairs_24h <- data.frame(pairs_names,pairs_24h_FEM,pairs_24h_Burnaby1,pairs_24h_Burnaby2,pairs_24h_Burnaby3)
names(pairs_24h) <- c("Monitor","FEM.24h","Burnaby1.24h","Burnaby2.24h","Burnaby3.24h")

pairs_combined <- full_join(pairs_1h,pairs_24h,by="Monitor")

# Print the result in a table
pairs_combined %>%
  kbl(caption="**# Paired Sensor-Sensor and Sensor-FEM Measurements**",col.names=c("","FEM","Burnaby1","Burnaby2","Burnaby3","FEM","Burnaby1","Burnaby2","Burnaby3")) %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  add_header_above(c(" " = 1, "1 h" = 4, "24 h" = 4))
# Paired Sensor-Sensor and Sensor-FEM Measurements
1 h
24 h
FEM Burnaby1 Burnaby2 Burnaby3 FEM Burnaby1 Burnaby2 Burnaby3
FEM NA 2865 2865 2865 NA 122 122 122
Burnaby1 2865 NA 2905 2905 122 NA 122 122
Burnaby2 2865 2905 NA 2905 122 122 NA 122
Burnaby3 2865 2905 2905 NA 122 122 122 NA
## 7. Number of paired 24-hour, normalized concentration and temperature values &
## 8. Number of paired 24-hour, normalized concentration and relative humidity values

# We will tabulate items 7-8 simultaneously. Note this section of code is a little clunky - apologies!

pairs_met_names <- c("Burnaby1","Burnaby2","Burnaby3")

# Get paired number of 24 h measurements averages using "complete.cases"
paired_24h_nB1.T <- sum(complete.cases(df_24h_all$norm_Burnaby1,df_24h_all$temp))
paired_24h_nB2.T <- sum(complete.cases(df_24h_all$norm_Burnaby2,df_24h_all$temp))
paired_24h_nB3.T <- sum(complete.cases(df_24h_all$norm_Burnaby3,df_24h_all$temp))

paired_24h_nB1.RH <- sum(complete.cases(df_24h_all$norm_Burnaby1,df_24h_all$rel_hum))
paired_24h_nB2.RH <- sum(complete.cases(df_24h_all$norm_Burnaby2,df_24h_all$rel_hum))
paired_24h_nB3.RH <- sum(complete.cases(df_24h_all$norm_Burnaby3,df_24h_all$rel_hum))

# Store the data and combine in a new data frame
pairs_T <- c(paired_24h_nB1.T,paired_24h_nB2.T,paired_24h_nB3.T)
pairs_RH <- c(paired_24h_nB1.RH,paired_24h_nB2.RH,paired_24h_nB3.RH)

pairs_met <- data.frame(pairs_met_names,pairs_T,pairs_RH)

# Print the result.
pairs_met %>%
  kbl(caption="**# 24 h Avg. Paired Normalized Concentration and Met Measurements**",col.names=c("Sensor","T","RH")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
# 24 h Avg. Paired Normalized Concentration and Met Measurements
Sensor T RH
Burnaby1 122 122
Burnaby2 122 122
Burnaby3 122 122
## 9. Sensor uptime (%) (1 h and 24 h)

# Determine the maximum number of observations
max_1h <- nrow(df_1h_precision)
max_24h <- nrow(df_24h_precision)

# Calculate uptime by adding up rows that are not 'NA' and dividing by max. # of observations
uptime.1h.B1 <- sum(!is.na(data_1h_Burnaby1)) / max_1h * 100
uptime.1h.B2 <- sum(!is.na(data_1h_Burnaby2)) / max_1h * 100
uptime.1h.B3 <- sum(!is.na(data_1h_Burnaby3)) / max_1h * 100
uptime.24h.B1 <- sum(!is.na(data_24h_Burnaby1)) / max_24h * 100
uptime.24h.B2 <- sum(!is.na(data_24h_Burnaby2)) / max_24h * 100
uptime.24h.B3 <- sum(!is.na(data_24h_Burnaby3)) / max_24h * 100

# Store the data and combine in a new data frame
uptime_1h <- c(uptime.1h.B1,uptime.1h.B2,uptime.1h.B3)
uptime_24h <- c(uptime.24h.B1,uptime.24h.B2,uptime.24h.B3)

uptime <- data.frame(pairs_met_names,uptime_1h,uptime_24h)

# Print the result.
uptime %>% 
  kbl(caption="**Sensor Uptime (%)**",col.names = c("Sensor","1 h","24 h")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Sensor Uptime (%)
Sensor 1 h 24 h
Burnaby1 100 100
Burnaby2 100 100
Burnaby3 100 100

We have now completed all of the main reporting metrics requested in the U.S. EPA Base Testing PM2.5 Report. From this analysis, we can conclude that without calibration, these sensors fail to meet some targets (RMSE, NRMSE, SD), but in general are well-correlated to both the regulatory monitoring data and each other. Looking at the data, this was likely skewed by the poorer performance of “Burnaby2” and the wildfire period (in fact the U.S. EPA cautions about very high concentration periods in your data set). This is summarized briefly below:

  • Accuracy:
    • R2: Pass. Target is > 0.7 and the mean across all sensors was 0.84 (1 h) and 0.93 (24 h)
    • Slope: Pass. Target is 1 +/- 0.35 and the mean across all sensors was 1.03 (1 h ) and 0.98 (24 h)
    • Intercept: Pass. Target is -5 to +5 and the mean across all sensors was 3.16 μg/m3 (1 h ) and 3.43 μg/m3 (24 h)
    • RMSE: Fail. Target is ≤ 7 μg/m3 and the mean across all sensors was 12.02 μg/m3 (1 h ) and 8.47 μg/m3 (24 h). This was influenced heavily by sensor “Burnaby2” which seems to have some issues.
    • NRMSE: Fail. Target is ≤ 30% and the mean across all sensors was 117.85% (1 h ) and 74.66% (24 h). This was likely influenced by the wildfire period.
  • Precision:
    • SD: Failed. Target is <5 μg/m3 and the mean across all sensors was 51.67 μg/m3 (1 h ) and 18.42 μg/m3 (24 h). This was likely skewed by the poorer performance of “Burnaby2” and the high concentrations during the wildfire period.
    • CV(%): Pass. Target is <30%. For both 1 h and 24 h calculations this was < 10%.
  • Remaining Tabular Statistics: All pass. Exceptional uptime.

Part 3: Building Calibration Models

The last thing we will consider is building a calibration model. Most low-cost PM sensors have relied on multiple linear regression with parameters such as temperature, relative humidity and dew point. Many low-cost sensors measure temperature and relative humidity, however, these are not always reliable (if they are affected by internal heating or conditions within the sensor), and so models should be built using external met stations if possible. Let’s consider building a calibration model for sensor “Burnaby1” at 1 hour time resolution and look at some techniques for assessing model performance.

This is mostly for demonstration purposes - the raw performance of Burnaby1 was reasonably good based on Part 2. However, illustrating some approaches is still useful. Let’s begin.

Prepare the data

Before we begin, we will need to extract the relevant data for model building and separate it into testing and training data sets. A typical testing-training split is 80:20. We will use the caret package to achieve this splitting (Kuhn 2020).

# We already downloaded the hourly weather data, we need to now join it to the 1 h data.

weather_1h <- rename(weather_1h,datetime_1h=date)
df_calibration <- full_join(df_1h_precision,weather_1h,by="datetime_1h")

# Let's select the columns we are interested in including in our multiple linear regression model

df_calibration <- df_calibration %>% select(FEM,Burnaby1,temp,rel_hum,temp_dew)

# We will only keep complete cases for model building (every column has a measurement.)

df_calibration <- df_calibration[complete.cases(df_calibration),]

# Make an index for splitting data into training and testing

data_split <- createDataPartition(df_calibration$FEM, times=1, list=F,p=0.8)
data_train <- df_calibration[data_split,]
data_test <- df_calibration[-data_split,]

Build sample models

As a demonstration, we will consider every linear combination of the sensor signal, the relative humidity and the dew point in our model building. We are excluding temperature as it was previously shown to have limited influence (Part 2, Meteoroloigcal influence). Given the results of separating the high concentration and low concentration and the change in slope (Part 2, Performance Metrics), we will also try three piecewise regression models. We will take advantage of the “segmented” package to determine the split point in PurpleAir monitor concentration (Muggeo 2021, 2003). We will duplicate models A, B and H using the piecewise regression approach.

A note: There are numerous options for calibration, but this will not be the focus of the tutorial. The primary goal is to illustrate how model performance can be assessed. As such, do not consider these models as your only option, just some example models to look at performance metrics.

# Model A: Including all parameters 
lm_model.A <- lm(FEM ~ Burnaby1 + rel_hum + temp_dew, data = data_train)

# Model B: Exclude dew point
lm_model.B <- lm(FEM ~ Burnaby1 + rel_hum, data = data_train)

# Model C: Exclude relative humidity
lm_model.C <- lm(FEM ~ Burnaby1 + temp_dew, data = data_train)

# Model D: Only include sensor as predictor of FEM
lm_model.D <- lm(FEM ~ Burnaby1, data = data_train)

## Let's also try some segmented (piecewise) models based on what we learned in Part 2. We will use the 'segmented' package, and use 1 break.

# Model E: Segmented model - just sensor and FEM monitor
lm_model.E <- segmented(lm(FEM ~ Burnaby1,data = data_train), seg.Z = ~Burnaby1, psi=NA, control = seg.control(K=1))

# Model F: Segmented model - Sensor + relative humidity
lm_model.F <- segmented(lm(FEM ~ Burnaby1+rel_hum,data = data_train), seg.Z = ~Burnaby1, psi=NA, control = seg.control(K=1))

# Model G: Segmented model = Sensor +  relative humidty + dew point
lm_model.G <- segmented(lm(FEM ~ Burnaby1+rel_hum+temp_dew,data = data_train), seg.Z = ~Burnaby1, psi=NA, control = seg.control(K=1))



# Predict the testing data concentrations with our models
predict_model.A <- predict(lm_model.A,data_test)
predict_model.B <- predict(lm_model.B,data_test)
predict_model.C <- predict(lm_model.C,data_test)
predict_model.D <- predict(lm_model.D,data_test)
predict_model.E <- predict(lm_model.E,data_test)
predict_model.F <- predict(lm_model.F,data_test)
predict_model.G <- predict(lm_model.G,data_test)

measured_test <- data_test %>% pull(FEM)

# Store the data
df_cal_fit <- data.frame(predict_model.A,predict_model.B,predict_model.C,predict_model.D,predict_model.E,predict_model.F,predict_model.G,measured_test)
df_cal_fit_long <- df_cal_fit %>% 
  pivot_longer(cols=starts_with("predict_"),names_to="model",names_prefix="predict_model.",values_to="predicted_test")
df_cal_fit_long <- setDT(df_cal_fit_long)

Assess models

We will assess models in 2 ways:

  • Taylor Diagrams which provide a quick visual snapshot of three metrics:
    • Standard deviation, which is measured based on radial distance to the marker
    • Correlation coefficient
    • Centered RMSE
  • Tabular data of R2 and standard error

The Taylor Diagram is easily constructed using the “TaylorDiagram” function in the R Package “openair” (Carslaw and Ropkins 2020). To read more about interpreting Taylor Diagrams please see the openair book, section 21.

summary.A <- glance(lm_model.A)
summary.B <- glance(lm_model.B)
summary.C <- glance(lm_model.C)
summary.D <- glance(lm_model.D)
summary.E <- glance(lm_model.E)
summary.F <- glance(lm_model.F)
summary.G <- glance(lm_model.G)

summary_all_models <- bind_rows(summary.A,summary.B,summary.C,summary.D,summary.E,summary.F,summary.G)  %>%
  mutate(modelID=c("A","B","C","D","E","F","G"))

taylor <- TaylorDiagram(df_cal_fit_long, obs = "measured_test", mod = "predicted_test", group = "model", main="Taylor Diagram")
zoomed <- TaylorDiagram(df_cal_fit_long, obs = "measured_test", mod = "predicted_test", group = "model",xlim=c(19,23),ylim=c(4,10), main="Zoomed In")

summary_all_models %>% select(modelID,r.squared,sigma) %>%
  kbl(caption="**Some Model Performance Metrics**",col.names = c("Model ID","R^2^","Standard Error"),digits=3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Some Model Performance Metrics
Model ID R2 Standard Error
A 0.910 7.353
B 0.907 7.463
C 0.910 7.368
D 0.907 7.480
E 0.942 5.912
F 0.942 5.896
G 0.942 5.897

From this, we can see that even without any corrections, the PurpleAir monitors are well correlated to the FEM monitors (model D). The segmented models (E-G) have notably better performance than the linear models across all concentration ranges, as was expected based on the Performance Metrics assessment split across 2 different concentration ranges.

The ‘segmented’ model automatically chooses the break point. Let’s see what break point it chose for models E-G.

breakpt.E <- summary.segmented(lm_model.E)$psi[2]
breakpt.F <- summary.segmented(lm_model.F)$psi[2]
breakpt.G <- summary.segmented(lm_model.G)$psi[2]
breakpts <- c(breakpt.E,breakpt.F,breakpt.G)
breakpts_ID <- c("Model E", "Model F", "Model G")
df_breakpoints <- data.frame(breakpts_ID,breakpts) 
df_breakpoints %>%
  kbl(caption="**Summary of breakpoint location (μg/m^3^)**",digits=2, col.names=c("Model ID","Breakpoint")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Summary of breakpoint location (µg/m3)
Model ID Breakpoint
Model E 31.68
Model F 31.56
Model G 31.57

As we can see, in all models, the break point is very similar, around 32 μg/m3. Let’s visualize this break point for our best performing model (model F). This will be the last step in our tutorial.

df.model.F <- data_train %>%
  mutate(seg.fit.F = broken.line(lm_model.F)$fit)

ggplot(df.model.F, aes(x = Burnaby1, y = FEM)) + 
  geom_point(alpha=0.5) +
  geom_line(aes(x = Burnaby1, y = seg.fit.F), color = 'red', linetype='dashed',size=1) +
  xlab(expression(Reported~Sensor~PM[2.5]~Concentration~(μg/m^3))) +
  ylab(expression(Measured~PM[2.5]~Concentration~(μg/m^3))) +
  ggtitle("Break point visualization: Sensor Burnaby1") +
  theme(plot.title = element_text(hjust = 0.5), text = element_text(size=12))

Congratulations! You have reached the end of the tutorial. I hope it was informative and feel free to contact me (see the “About” page) if you have any questions.

References

Allaire, JJ, Yihui Xie, Jonathan McPherson, Javier Luraschi, Kevin Ushey, Aron Atkins, Hadley Wickham, Joe Cheng, Winston Chang, and Richard Iannone. 2020. Rmarkdown: Dynamic Documents for r. https://github.com/rstudio/rmarkdown.
Callahan, Jonathan, Hans Martin, Spencer Pease, Helen Miller, Zach Dingels, Rohan Aras, Jon Hagg, Jimin Kim, Rex Thompson, and Alice Yang. 2020. PWFSLSmoke: Utilities for Working with Air Quality Monitoring Data. https://CRAN.R-project.org/package=PWFSLSmoke.
Callahan, Jonathan, Hans Martin, Kayleigh Wilson, Tate Brasel, and Helen Miller. 2020. AirSensor: Process and Display Data from Air Quality Sensors. https://github.com/MazamaScience/AirSensor.
Carslaw, David, and Karl Ropkins. 2020. Openair: Tools for the Analysis of Air Pollution Data. https://davidcarslaw.github.io/openair/.
Dowle, Matt, and Arun Srinivasan. 2020. Data.table: Extension of ‘Data.frame‘. https://CRAN.R-project.org/package=data.table.
Duvall, R., A. Clements, G. Hagler, A. Kamal, Vasu J. Kilaru, L. Goodman, S. Frederick, et al. 2021. Performance Testing Protocols, Metrics, and Target Values for Fine Particulate Matter Air Sensors: Use in Ambient, Outdoor, Fixed Site, Non-Regulatory Supplemental and Informational Monitoring Applications.” Washington, D.C.: U.S. EPA Office of Research; Development. https://cfpub.epa.gov/si/si_public_record_Report.cfm?dirEntryId=350785&Lab=CEMM.
Feenstra, Brandon, Ashley Collier-Oxandale, Vasileios Papapostolou, David Cocker, and Andrea Polidori. 2020. “The AirSensor Open-Source r-Package and DataViewer Web Application for Interpreting Community Data Collected by Low-Cost Sensor Networks.” Environmental Modelling & Software 134: 104832. https://doi.org/https://doi.org/10.1016/j.envsoft.2020.104832.
Galili, Tal. 2019. Installr: Using r to Install Stuff on Windows OS (Such as: R, Rtools, RStudio, Git, and More!). https://CRAN.R-project.org/package=installr.
Gerds, Thomas A. 2019. Prodlim: Product-Limit Estimation for Censored Event History Analysis. https://CRAN.R-project.org/package=prodlim.
James, David, and Kurt Hornik. 2020. Chron: Chronological Objects Which Can Handle Dates and Times. https://CRAN.R-project.org/package=chron.
Kuhn, Max. 2020. Caret: Classification and Regression Training. https://github.com/topepo/caret/.
LaZerte, Steffi. 2021. Weathercan: Download Weather Data from Environment and Climate Change Canada. https://CRAN.R-project.org/package=weathercan.
Malings, Carl, Rebecca Tanzer, Aliaksei Hauryliuk, Provat K Saha, Allen L Robinson, Albert A Presto, and R Subramanian. 2020. Fine particle mass monitoring with low-cost sensors: Corrections and long-term performance evaluation.” Aerosol Science and Technology 54 (2): 160–74. https://doi.org/10.1080/02786826.2019.1623863.
Muggeo, Vito M. R. 2003. “Estimating Regression Models with Unknown Break-Points.” Statistics in Medicine 22: 3055–71.
———. 2021. “Segmented: An r Package to Fit Regression Models with Broken-Line Relationships.” https://CRAN.R-project.org/package=segmented.
Robinson, David, Alex Hayes, and Simon Couch. 2020. Broom: Convert Statistical Objects into Tidy Tibbles. https://CRAN.R-project.org/package=broom.
RStudio Team. 2020. RStudio: Integrated Development Environment for r. Boston, MA: RStudio, PBC. http://www.rstudio.com/.
Warnes, Gregory R., Ben Bolker, and Thomas Lumley. 2020. Gtools: Various r Programming Tools. https://github.com/r-gregmisc/gtools.
Wickham, Hadley. 2019. Tidyverse: Easily Install and Load the Tidyverse. https://CRAN.R-project.org/package=tidyverse.
Yong, Zhou. 2016. Digital universal particle concentration sensor, PMS5003 series data manual.” http://www.aqmd.gov/docs/defaultsource/%0Aaq-spec/resources-page/plantower-pms5003-manual_v2-3.pdf.
Zhu, Hao. 2020. kableExtra: Construct Complex Table with Kable and Pipe Syntax. https://CRAN.R-project.org/package=kableExtra.
LS0tDQp0aXRsZTogIkxvdy1Db3N0IFBNIFNlbnNvciBUdXRvcmlhbCINCmF1dGhvcjogIkRyLiBOYW9taSBaaW1tZXJtYW4iDQpkYXRlOiAiTGFzdCBVcGRhdGVkOiBgciBTeXMuRGF0ZSgpYCINCm91dHB1dDogDQogIGh0bWxfZG9jdW1lbnQ6DQogICAgdG9jOiB0cnVlDQogICAgdG9jX2Zsb2F0OiB0cnVlDQogICAgdGhlbWU6IHlldGkNCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBzaG93DQogICAga2VlcF90ZXg6IHllcw0KYmlibGlvZ3JhcGh5OiBiaWJsaW8uYmliDQpsaW5rLWNpdGF0aW9uczogeWVzDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoDQogIGVjaG8gPSBUUlVFLCANCiAgZmlnLndpZHRoID0gNywNCiAgZmlnLmFzcCA9IDAuOCwNCiAgbWVzc2FnZSA9IEZBTFNFLA0KICB3YXJuaW5nID0gRkFMU0UNCikNCmBgYA0KDQojIEludHJvZHVjdGlvbg0KDQpUaGlzIHR1dG9yaWFsIGJyb2FkbHkgY292ZXJzIHZhbGlkYXRpb24gYXBwcm9hY2hlcyBmb3IgbG93LWNvc3QgUE1+Mi41fiBzZW5zb3IgZGF0YS4gSXQgY2FuIGJlIGRpc3RpbGxlZCBpbnRvIHRocmVlIG1haW4gc2VjdGlvbnM6DQoNCjEuIERvd25sb2FkaW5nIHNhbXBsZSBkYXRhIGZyb20gdGhlIFB1cnBsZSBBaXIgTmV0d29yaw0KMi4gQXNzZXNzaW5nIHRoaXMgZGF0YSBiYXNlZCBvbiB0aGUgbWV0cmljcyBvdXRsaW5lZCBpbiB0aGUgMjAyMSBVLlMuIEVQQSBCYXNlIFRlc3RpbmcgUmVwb3J0IFtARHV2YWxsMjAyMV0NCjMuIENvbnNpZGVyaW5nIHNvbWUgYXBwcm9hY2hlcyBmb3IgY2FsaWJyYXRpb24gbW9kZWxzIGFuZCBhc3Nlc3NpbmcgdGhlaXIgcGVyZm9ybWFuY2UNCg0KVGhpcyB0dXRvcmlhbCBpcyBhIGNvbXBhbmlvbiB0byB0aGUgX0pvdXJuYWwgb2YgQWVyb3NvbCBTY2llbmNlXyBhcnRpY2xlICJUdXRvcmlhbDogR3VpZGVsaW5lcyBmb3IgSW1wbGVtZW50aW5nIExvdy1Db3N0IFNlbnNvciBOZXR3b3JrcyBmb3IgQWVyb3NvbCBNb25pdG9yaW5nIi4gV2hlbiB1c2luZyB0aGlzIGRhdGEgb3IgY29kZSBwbGVhc2UgY2l0ZSB0aGUgY29ycmVzcG9uZGluZyBwYXBlcjoNCg0KKipOLiBaaW1tZXJtYW4qKiwgKDIwMjEpLiAiVHV0b3JpYWw6IEd1aWRlbGluZXMgZm9yIEltcGxlbWVudGluZyBMb3ctQ29zdCBTZW5zb3IgTmV0d29ya3MgZm9yIEFlcm9zb2wgTW9uaXRvcmluZyIuIF9Kb3VybmFsIG9mIEFlcm9zb2wgU2NpZW5jZV8sIGluIHByZXNzLiAoaHR0cHM6Ly9kb2kub3JnLzEwLjEwMTYvai5qYWVyb3NjaS4yMDIxLjEwNTg3MikNCg0KT25lIGNhbiBhbHNvIGNvbnNpZGVyIGNpdGluZyB0aGlzIHdlYiBhcHAgZGlyZWN0bHk6DQoNCk4uIFppbW1lcm1hbi4gIkxvdy1Db3N0IFBNIFNlbnNvciBUdXRvcmlhbC4iIChodHRwczovL256aW1tZXJtYW4tdWJjLmdpdGh1Yi5pby9sY3NfUE1fZGVtby8pLiANCg0KSW4gdGhlIHRvcC1yaWdodCBjb3JuZXIgb2YgdGhpcyBwYWdlIGlzIGEgYnV0dG9uIGxhYmVsZWQgIkNvZGUiIHdoaWNoIHdpbGwgYWxsb3cgeW91IHRvIGRvd25sb2FkIHRoZSBSIE1hcmtkb3duIGZpbGUgdXNlZCB0byBnZW5lcmF0ZSB0aGlzIHByb2plY3QuIFRvIHJ1biB0aGUgY29kZSBvbiB5b3VyIG93biBtYWNoaW5lLCB5b3Ugd2lsbCBuZWVkIHRvIGRvd25sb2FkIGFuZCBpbnN0YWxsIGJvdGggUlN0dWRpbyBbQFJTdHVkaW9dIGFuZCB0aGUgcGFja2FnZSAicm1hcmtkb3duIiBbQFItcm1hcmtkb3duXS4gU2VlIFtoZXJlXShodHRwczovL3JtYXJrZG93bi5yc3R1ZGlvLmNvbS9sZXNzb24tMS5odG1sKSB0byBsZWFybiBtb3JlLg0KDQojIFJlcXVpcmVkIHBhY2thZ2VzDQoNCkJlZm9yZSBiZWdpbm5pbmcsIHdlIHdpbGwgbmVlZCB0byBpbnN0YWxsIGEgbnVtYmVyIG9mIHBhY2thZ2VzIHRvIG1ha2UgdGhpcyBjb2RlIHJ1biBbQFItaW5zdGFsbHI7QFItY2hyb247QFItcHJvZGxpbTtAUi1ndG9vbHM7QFItQWlyU2Vuc29yO0BSLWNhcmV0O0BSLWRhdGEudGFibGU7QFItdGlkeXZlcnNlO0BSLW9wZW5haXI7QFIta2FibGVFeHRyYTtAUi1icm9vbTtAUi13ZWF0aGVyY2FuO0BSLXNlZ21lbnRlZF0uIFRoaXMgY29kZSBjaHVuayB3aWxsIGNoZWNrIGlmIHlvdSBoYXZlIHRoZXNlIHBhY2thZ2VzIGluc3RhbGxlZC4gSWYgeW91IGRvIG5vdCBoYXZlIHRoZW0gaW5zdGFsbGVkLCBpdCB3aWxsIGluc3RhbGwgdGhlbS4gSWYgdGhleSBhcmUgYWxyZWFkeSBpbnN0YWxsZWQsIHRoZSBjb2RlIHdpbGwgbG9hZCB0aGVtLiANCg0KYGBge3IgcGFja2FnZXN9DQppZiAoIXJlcXVpcmUoaW5zdGFsbHIpKSBpbnN0YWxsLnBhY2thZ2VzKCdpbnN0YWxscicpDQpsaWJyYXJ5KGluc3RhbGxyKQ0KDQppZiAoIXJlcXVpcmUodGlkeXZlcnNlKSkgaW5zdGFsbC5wYWNrYWdlcygndGlkeXZlcnNlJykNCmxpYnJhcnkodGlkeXZlcnNlKQ0KDQppZiAoIXJlcXVpcmUoY2hyb24pKSBpbnN0YWxsLnBhY2thZ2VzKCdjaHJvbicpDQpsaWJyYXJ5KGNocm9uKQ0KDQppZiAoIXJlcXVpcmUocHJvZGxpbSkpIGluc3RhbGwucGFja2FnZXMoJ3Byb2RsaW0nKQ0KbGlicmFyeShwcm9kbGltKQ0KDQppZiAoIXJlcXVpcmUoZ3Rvb2xzKSkgaW5zdGFsbC5wYWNrYWdlcygnZ3Rvb2xzJykNCmxpYnJhcnkoZ3Rvb2xzKQ0KDQppZiAoIXJlcXVpcmUoQWlyU2Vuc29yKSkgaW5zdGFsbC5wYWNrYWdlcygnQWlyU2Vuc29yJykNCmxpYnJhcnkoQWlyU2Vuc29yKQ0KDQppZiAoIXJlcXVpcmUoY2FyZXQpKSBpbnN0YWxsLnBhY2thZ2VzKCdjYXJldCcpDQpsaWJyYXJ5KGNhcmV0KQ0KDQppZiAoIXJlcXVpcmUoZGF0YS50YWJsZSkpIGluc3RhbGwucGFja2FnZXMoJ2RhdGEudGFibGUnKQ0KbGlicmFyeShkYXRhLnRhYmxlKQ0KDQppZiAoIXJlcXVpcmUob3BlbmFpcikpIGluc3RhbGwucGFja2FnZXMoJ29wZW5haXInKQ0KbGlicmFyeShvcGVuYWlyKQ0KDQppZiAoIXJlcXVpcmUoa2FibGVFeHRyYSkpIGluc3RhbGwucGFja2FnZXMoJ2thYmxlRXh0cmEnKQ0KbGlicmFyeShrYWJsZUV4dHJhKQ0KDQppZiAoIXJlcXVpcmUoYnJvb20pKSBpbnN0YWxsLnBhY2thZ2VzKCdicm9vbScpDQpsaWJyYXJ5KGJyb29tKQ0KDQppZiAoIXJlcXVpcmUod2VhdGhlcmNhbikpIGluc3RhbGwucGFja2FnZXMoJ3dlYXRoZXJjYW4nKQ0KbGlicmFyeSh3ZWF0aGVyY2FuKQ0KDQppZiAoIXJlcXVpcmUoc2VnbWVudGVkKSkgaW5zdGFsbC5wYWNrYWdlcygnc2VnbWVudGVkJykNCmxpYnJhcnkoc2VnbWVudGVkKQ0KDQpgYGANCg0KIyBQYXJ0IDE6IERvd25sb2FkaW5nIFNhbXBsZSBEYXRhDQoNClRvIGlsbHVzdHJhdGUgaG93IHRvIHdvcmsgd2l0aCBsb3ctY29zdCBQTSBzZW5zb3IgZGF0YSwgd2Ugd2lsbCB0YWtlIGFkdmFudGFnZSBvZiBhIGxhcmdlLCBlc3RhYmxpc2hlZCBuZXR3b3JrOiBbUHVycGxlQWlyXShodHRwczovL3d3dzIucHVycGxlYWlyLmNvbS8pLiBUaGUgdHV0b3JpYWwgd2lsbCBjb25zaWRlciBzb21lIHNlbnNvcnMgZGVwbG95ZWQgaW4gdGhlIEdyZWF0ZXIgVmFuY291dmVyIEFyZWEsIGJ1dCB0aGlzIGNvZGUgY291bGQgYmUgZWFzaWx5IGFkb3B0ZWQgdG8gZGlmZmVyZW50IFB1cnBsZUFpciBzZW5zb3JzIGJ5IGFkanVzdGluZyB0aGUgc2Vuc29yIGlkZW50aWZpZXIuIA0KDQpVc2luZyB0aGUgUHVycGxlQWlyIG5ldHdvcmsgaW4gdGhpcyB0dXRvcmlhbCBpcyBjb252ZW5pZW50LCBzaW5jZSB0aGUgUiBQYWNrYWdlICJBaXJTZW5zb3IiIFtAUi1BaXJTZW5zb3JdIGhhcyBkZXZlbG9wZWQgZWFzaWx5IGltcGxlbWVudGFibGUgdG9vbHMgZm9yIGRvd25sb2FkaW5nIFB1cnBsZUFpciBkYXRhIGZyb20gdGhlaXIgbmV0d29yay4gVGhlIGRldmVsb3BtZW50IG9mIHRoZSBBaXJTZW5zb3IgcGFja2FnZSBpcyBhbHNvIGRldGFpbGVkIGJ5IEZlZW5zdHJhIGV0IGFsLiBbQEZlZW5zdHJhMjAyMF0uDQoNCiMjIFNldCB1cCBhIGxvY2FsIGRpcmVjdG9yeQ0KDQpUaGUgQWlyU2Vuc29yIHBhY2thZ2UgbmVlZHMgdG8ga25vdyB3aGVyZSBwcm9jZXNzZWQgZGF0YSB3aWxsIGxpdmUuIEZvciB0aGlzIHJlcG9ydCwgd2Ugd2lsbCBzcGVjaWZ5IGEgbG9jYWwgYXJjaGl2ZUJhc2VEaXIgd2hlcmUgZG93bmxvYWRlZCBhbmQgcHJvY2Vzc2VkIGRhdGEgd2lsbCBsaXZlLiBUaGUgZm9sbG93aW5nIGNvZGUgc3BlY2lmaWVzIGEgbG9jYWwgZGlyZWN0b3J5IGFyY2hpdmUsIGNoZWNrcyBmb3IgaXRzIGV4aXN0ZW5jZSBhbmQgY3JlYXRlcyBpdCBpZiBuZWVkZWQuIEl0IGFsc28gY2hlY2tzIHRvIHNlZSBpZiB5b3UgaGF2ZSBkb3dubG9hZGVkIHRoZSByZXF1aXJlZCBzcGF0aWFsIGRhdGEgc2V0cyAodG8gZXh0cmFjdCBQdXJwbGVBaXIgZGF0YSBieSBDb3VudHJ5IENvZGUpIGFuZCBpbnN0YWxscyB0aGVtIGlmIG5lY2Vzc2FyeS4gVGhpcyBjaHVuayBvZiBjb2RlIHdhcyBkZXZlbG9wZWQgYmFzZWQgb24gdGhlIEFydGljbGUgWyJBdXN0cmFsaWEgb24gRmlyZSJdKGh0dHBzOi8vbWF6YW1hc2NpZW5jZS5naXRodWIuaW8vQWlyU2Vuc29yL2FydGljbGVzL2FydGljbGVzL0F1c3RyYWxpYV9vbl9maXJlLmh0bWwpIGJ5IE1hemFtYSBTY2llbmNlLiANCg0KYGBge3Igc2V0X3VwfQ0KDQojIyBDcmVhdGUgYSBmb2xkZXIgaW4geW91ciBSIGhvbWUgZGlyZWN0b3J5IGZvciB0aGlzIHR1dG9yaWFsIGNhbGxlZCAnUE1fU2Vuc29yX1R1dG9yaWFsJw0KbmV3X3dkIDwtIHBhdGguZXhwYW5kKCJ+L1BNX1NlbnNvcl9UdXRvcmlhbCIpDQppZiAoICFkaXIuZXhpc3RzKG5ld193ZCkgKSB7DQogIGRpci5jcmVhdGUobmV3X3dkKQ0KfQ0Kc2V0d2QobmV3X3dkKQ0KDQojIyBDcmVhdGUgYSBmb2xkZXIgaW4gJ1BNX1NlbnNvcl9UdXRvcmlhbCcgZm9yIHN0b3JpbmcgdGhlIGRvd25sb2FkZWQgUHVycGxlQWlyIGRhdGEuDQphcmNoaXZlQmFzZURpciA8LSBwYXRoLmV4cGFuZCgifi9QTV9TZW5zb3JfVHV0b3JpYWwvRGF0YSIpDQppZiAoICFkaXIuZXhpc3RzKGFyY2hpdmVCYXNlRGlyKSApIHsNCiAgZGlyLmNyZWF0ZShhcmNoaXZlQmFzZURpcikNCn0NCnNldEFyY2hpdmVCYXNlRGlyKGFyY2hpdmVCYXNlRGlyKQ0KDQojIyBDaGVja2luZyB0byBzZWUgaWYgeW91IGhhdmUgdGhlIHJlcXVpcmVkIHNwYXRpYWwgZGF0YSwgYW5kIGluc3RhbGxpbmcgaXQgdW4gYSBzdWItZm9sZGVyIG9mIFBNX1NlbnNvcl9UdXRvcmlhbC9EYXRhL1NwYXRpYWwgaWYgbmVlZGVkDQppZiAoIXJlcXVpcmUoTWF6YW1hU3BhdGlhbFV0aWxzKSkgaW5zdGFsbC5wYWNrYWdlcygnTWF6YW1hU3BhdGlhbFV0aWxzJykNCmxpYnJhcnkoTWF6YW1hU3BhdGlhbFV0aWxzKQ0KDQppZiAoICFkaXIuZXhpc3RzKCd+L1BNX1NlbnNvcl9UdXRvcmlhbC9EYXRhL1NwYXRpYWwnKSApIHsNCiAgZGlyLmNyZWF0ZSgnfi9QTV9TZW5zb3JfVHV0b3JpYWwvRGF0YS9TcGF0aWFsJykNCiAgc2V0U3BhdGlhbERhdGFEaXIoJ34vUE1fU2Vuc29yX1R1dG9yaWFsL0RhdGEvU3BhdGlhbCcpDQogIGluc3RhbGxTcGF0aWFsRGF0YSgpDQogIGluc3RhbGxTcGF0aWFsRGF0YSgnTmF0dXJhbEVhcnRoQWRtMScpDQp9DQpgYGANCg0KIyMgTG9hZCBzeW5vcHRpYyBkYXRhIGZvciBDYW5hZGENCg0KV2Ugd2lsbCB1c2UgdGhlIEFpclNlbnNvciBwYXNfY3JlYXRlTmV3KCkgZnVuY3Rpb24gdG8gY3JlYXRlIGEgUHVycGxlQWlyIFN5bm9wdGljIChwYXMpIG9iamVjdCBjb250YWluaW5nIGFsbCB0aGUgc3BhdGlhbCBtZXRhZGF0YSBhc3NvY2lhdGVkIHdpdGggUHVycGxlQWlyIG1vbml0b3JzIGluIENhbmFkYSAod2Ugd2lsbCBjYWxsIHRoaXMgb2JqZWN0ICdwYXNfQ0FOJykuIFNlZSBbdGhpcyBNYXphbWEgU2NpZW5jZSBwYWdlXShodHRwczovL21hemFtYXNjaWVuY2UuZ2l0aHViLmlvL0FpclNlbnNvci9hcnRpY2xlcy9wYXNfaW50cm9kdWN0aW9uLmh0bWwvKSBmb3IgbW9yZSBkZXRhaWxzLg0KDQpUaGUgZm9sbG93aW5nIGNvZGUgY2hlY2tzIHRvIG1ha2Ugc3VyZSB0aGUgcGFzX0NBTiBmaWxlIGV4aXN0cyBpbiB5b3VyIGFyY2hpdmVCYXNlRGlyIGRpcmVjdG9yeSBhbmQsIGlmIG1pc3NpbmcsIHJlY3JlYXRlcyBpdC4NCg0KYGBge3IgZGF0YV9kb3dubG9hZH0NCmZpbGVQYXRoX3BhcyA8LSBmaWxlLnBhdGgoYXJjaGl2ZUJhc2VEaXIsICJwYXNfQ0FOLnJkYSIpDQoNCmlmICggIWZpbGUuZXhpc3RzKGZpbGVQYXRoX3BhcykgKSB7DQogIGluaXRpYWxpemVNYXphbWFTcGF0aWFsVXRpbHMoc3BhdGlhbERhdGFEaXIgPSAifi9QTV9TZW5zb3JfVHV0b3JpYWwvRGF0YS9TcGF0aWFsIikNCiAgcGFzX0NBTiA8LSBwYXNfY3JlYXRlTmV3KGNvdW50cnlDb2RlcyA9ICJDQSIsIGluY2x1ZGVQV0ZTTCA9IFRSVUUsIGxvb2tiYWNrRGF5cyA9IDEsYmFzZVVybCA9ICJodHRwczovL3d3dy5wdXJwbGVhaXIuY29tL2pzb24/YWxsPXRydWUiKQ0KICBzYXZlKHBhc19DQU4sIGZpbGUgPSBmaWxlUGF0aF9wYXMpDQp9DQoNCnBhc19DQU4gPC0gZ2V0KGxvYWQoZmlsZVBhdGhfcGFzKSkNCmBgYA0KDQojIyBWaXN1YWxpemUgc25hcHNob3Qgb2YgQWlyIFF1YWxpdHkgaW4gQ2FuYWRhDQoNCk5vdyB3ZSB3aWxsIHByaW50IGEgbWFwIG9mIHRoZSByZXN1bHRzLiBUaGlzIGlzIGFsbCB0aGUgUHVycGxlQWlyIHVuaXRzIGN1cnJlbnRseSBvbmxpbmUgaW4gQ2FuYWRhLiBJZiB5b3UgY2xpY2sgb24gYSBtYXJrZXIsIGl0IHdpbGwgcG9wIHVwIHRoZSBtb25pdG9yIGRldGFpbHMgKG1vc3QgcmVjZW50IHJlYWRpbmcsIHN0YXRpb24gbmFtZSwgZXRjLikuIFRoZSBtYXJrZXJzIGFyZSBjb2xvdXJlZCBieSBbQWlyIFF1YWxpdHkgSW5kZXggKEFRSSldKGh0dHBzOi8vd3d3LmFpcm5vdy5nb3YvYXFpL2FxaS1iYXNpY3MvKS4NCmBgYHtyIG1hcH0NCnBhc19sZWFmbGV0KHBhc19DQU4pDQpgYGANCg0KIyMgRG93bmxvYWQgb3VyIFNhbXBsZSBEYXRhDQoNCk1ldHJvIFZhbmNvdXZlciwgdGhlIGxvY2FsIHJlZ3VsYXRvcnkgYWdlbmN5LCBoYXMgdHdvIFB1cnBsZUFpciBtb25pdG9ycyBjby1sb2NhdGVkIHdpdGggdGhlaXIgJ0J1cm5hYnkgU291dGgnIFJlZ3VsYXRvcnkgTW9uaXRvcmluZyBTaXRlIFtodHRwczovL2dpcy5tZXRyb3ZhbmNvdXZlci5vcmcvbWFwcy9BaXJdLiBUaGlzIGlzIGEgZ29vZCBzaXRlIGZvciBvdXIgdHV0b3JpYWwsIGFzIHRoZXJlIGlzIGJvdGggcmVndWxhdG9yeSBkYXRhIGFuZCBsb3ctY29zdCBzZW5zb3IgZGF0YSBhdmFpbGFibGUuIFRoZSBVLlMuIEVQQSByZWNvbW1lbmRzIHRoYXQgd2hlbiBhc3Nlc3Npbmcgc2Vuc29yIHBlcmZvcm1hbmNlLCBhIG1pbmltdW0gb2YgdGhyZWUgY28tbG9jYXRlZCBzZW5zb3JzIGJlIGFzc2Vzc2VkIFtARHV2YWxsMjAyMV0uIFdoaWxlIHRoaXMgaXMgbm90IGV4YWN0bHkgcG9zc2libGUgaGVyZSwgd2Ugd2lsbCBhbHNvIHNjcmFwZSBkYXRhIGZyb20gYW4gYWRkaXRpb25hbCBQdXJwbGVBaXIgbW9uaXRvciBsb2NhdGVkIGFwcHJveGltYXRlbHkgMSBrbSBlYXN0IG9mIHRoZSBNZXRybyBWYW5jb3V2ZXIgQnVybmFieSBTb3V0aCBSZWd1bGF0b3J5IE1vbml0b3JpbmcgU3RhdGlvbi4gV2Ugd2lsbCBzdG9yZSB0aGlzIGRhdGEgYXMgUHVycGxlQWlyIFRpbWUgU2VyaWVzIChwYXQpIG9iamVjdHMuIA0KDQpTdW1tYXJ5IFB1cnBsZUFpciBtb25pdG9yIGxvY2F0aW9ucyBhbmQgbmFtaW5nIGNvbnZlbnRpb24gdXNlZCBoZXJlOg0KDQoqICoqcGF0X0J1cm5hYnkxKio6IFB1cnBsZUFpciBJRDogIk5BUFNJRDogMTAwMTE5IC0gMSIsIGxvY2F0ZWQgYXQ6IEJ1cm5hYnkgU291dGggUmVndWxhdG9yeSBNb25pdG9yaW5nIFN0YXRpb24uDQoqICoqcGF0X0J1cm5hYnkyKio6IFB1cnBsZUFpciBJRDogIk5BUFNJRDogMTAwMTE5IC0gMiIsIGxvY2F0ZWQgYXQ6IEJ1cm5hYnkgU291dGggUmVndWxhdG9yeSBNb25pdG9yaW5nIFN0YXRpb24uDQoqICoqcGF0X0J1cm5hYnkzKio6IFB1cnBsZUFpciBJRDogIk5BUFNJRDogMTAwMTE5IC0gMyIsIGxvY2F0ZWQ6IH4gMSBrbSBlYXN0IG9mIHRoZSBCdXJuYWJ5IFNvdXRoIFJlZ3VsYXRvcnkgTW9uaXRvcmluZyBTdGF0aW9uLg0KDQpXZSB3aWxsIGRvd25sb2FkIGRhdGEgZnJvbSBTZXB0ZW1iZXIgMSAyMDIwIC0gRGVjZW1iZXIgMzEgMjAyMCAoNCBtb250aHMgLSBpbmNsdWRpbmcgYSB3aWxkZmlyZSBzbW9rZSBlcGlzb2RlKS4NCg0KYGBge3IgdGltZV9zZXJpZXNfZG93bmxvYWR9DQpmaWxlUGF0aF9CdXJuYWJ5MSA8LSBmaWxlLnBhdGgoYXJjaGl2ZUJhc2VEaXIsICJwYXRfQnVybmFieTEucmRhIikNCg0KaWYgKCBmaWxlLmV4aXN0cyhmaWxlUGF0aF9CdXJuYWJ5MSkgKSB7DQogIHBhdF9CdXJuYWJ5MSA8LSBnZXQobG9hZChmaWxlUGF0aF9CdXJuYWJ5MSkpDQp9IGVsc2Ugew0KICBwYXRfQnVybmFieTEgPC0gcGF0X2NyZWF0ZU5ldygNCiAgICBwYXMgPSBwYXNfQ0FOLCANCiAgICBsYWJlbCA9ICJOQVBTSUQ6IDEwMDExOSAtIDEiLCANCiAgICBzdGFydGRhdGUgPSAyMDIwMDkwMSwgDQogICAgZW5kZGF0ZSA9IDIwMjAxMjMxDQogICkNCiAgc2F2ZShwYXRfQnVybmFieTEsIGZpbGUgPSBmaWxlUGF0aF9CdXJuYWJ5MSkNCn0NCg0KZmlsZVBhdGhfQnVybmFieTIgPC0gZmlsZS5wYXRoKGFyY2hpdmVCYXNlRGlyLCAicGF0X0J1cm5hYnkyLnJkYSIpDQoNCmlmICggZmlsZS5leGlzdHMoZmlsZVBhdGhfQnVybmFieTIpICkgew0KICBwYXRfQnVybmFieTIgPC0gZ2V0KGxvYWQoZmlsZVBhdGhfQnVybmFieTIpKQ0KfSBlbHNlIHsNCiAgcGF0X0J1cm5hYnkyIDwtIHBhdF9jcmVhdGVOZXcoDQogICAgcGFzID0gcGFzX0NBTiwgDQogICAgbGFiZWwgPSAiTkFQU0lEOiAxMDAxMTktMiIsIA0KICAgIHN0YXJ0ZGF0ZSA9IDIwMjAwOTAxLCANCiAgICBlbmRkYXRlID0gMjAyMDEyMzENCiAgKQ0KICBzYXZlKHBhdF9CdXJuYWJ5MiwgZmlsZSA9IGZpbGVQYXRoX0J1cm5hYnkyKQ0KfQ0KDQpmaWxlUGF0aF9CdXJuYWJ5MyA8LSBmaWxlLnBhdGgoYXJjaGl2ZUJhc2VEaXIsICJwYXRfQnVybmFieTMucmRhIikNCg0KaWYgKCBmaWxlLmV4aXN0cyhmaWxlUGF0aF9CdXJuYWJ5MykgKSB7DQogIHBhdF9CdXJuYWJ5MyA8LSBnZXQobG9hZChmaWxlUGF0aF9CdXJuYWJ5MykpDQp9IGVsc2Ugew0KICBwYXRfQnVybmFieTMgPC0gcGF0X2NyZWF0ZU5ldygNCiAgICBwYXMgPSBwYXNfQ0FOLCANCiAgICBsYWJlbCA9ICJOQVBTSUQ6IDEwMDExOS0zIiwgDQogICAgc3RhcnRkYXRlID0gMjAyMDA5MDEsIA0KICAgIGVuZGRhdGUgPSAyMDIwMTIzMQ0KICApDQogIHNhdmUocGF0X0J1cm5hYnkzLCBmaWxlID0gZmlsZVBhdGhfQnVybmFieTMpDQp9DQpgYGANCg0KIyMgTWFwIG9mIFB1cnBsZUFpciBtb25pdG9ycw0KDQpUbyB2aXN1YWxpemUgdGhlIGxvY2F0aW9uIG9mIHRoZXNlIFB1cnBsZUFpciBtb25pdG9ycywgd2UgY2FuIHVzZSB0aGUgInBhc19sZWFmbGV0IiBmdW5jdGlvbiBhZ2Fpbi4gTm90ZSB0aGF0IHlvdSBjYW4gc2VlIHRoZSB0d28gUHVycGxlQWlyIHNlbnNvcnMgb24gdGhlIHJvb2Ygb2YgdGhlIEJ1cm5hYnkgU291dGggc3RhdGlvbiBpZiB5b3Ugem9vbSBpbi4NCg0KYGBge3IgbWFwMiwgZWNobz1GQUxTRX0NCmxvbiA8LSBwYXRfQnVybmFieTEkbWV0YSRsb25naXR1ZGUNCmxhdCA8LSBwYXRfQnVybmFieTEkbWV0YSRsYXRpdHVkZQ0KcGFzX3ZhbmNvdXZlciA8LSANCiAgcGFzX0NBTiAlPiUNCiAgcGFzX2ZpbHRlck5lYXIoDQogICAgbG9uZ2l0dWRlID0gbG9uLCANCiAgICBsYXRpdHVkZSA9IGxhdCwgDQogICAgcmFkaXVzID0gIjEga20iDQogICkgDQpwYXNfbGVhZmxldChwYXNfdmFuY291dmVyKQ0KYGBgDQoNCiMjIEJ1aWx0LWluIHZhbGlkYXRpb24gaW4gIkFpclNlbnNvciINCg0KQnVpbHQgaW50byB0aGUgQWlyU2Vuc29yIHBhY2thZ2UgaXMgYSB2YWxpZGF0aW9uIGZ1bmN0aW9uLCAicGF0X2V4dGVybmFsRml0Ii4gVGhpcyBmdW5jdGlvbiBpZGVudGlmaWVzIHRoZSBuZWFyZXN0IHJlZ3VsYXRvcnkgbW9uaXRvcmluZyBkYXRhIHZpYSB0aGUgVVNGUyBQYWNpZmljIFdpbGRsYW5kIEZpcmUgU2NpZW5jZXMgTGFiJ3MgKFBXRlNMKSBQV0ZTTFNtb2tlIHBhY2thZ2UgW0BSLVBXRlNMU21va2VdLiBTb3VyY2VzIG9mIHJlZ3VsYXRvcnkgUE1+Mi41fiBkYXRhIG1hZGUgYWNjZXNzaWJsZSBieSB0aGUgcGFja2FnZSBpbmNsdWRlOiBkYXRhIGZyb20gdGhlIFVuaXRlZCBTdGF0ZXMgRW52aXJvbm1lbnRhbCBQcm90ZWN0aW9uIEFnZW5jeSAoVVMgRVBBKSBhbmQgaXRzIEFpck5vdyBhaXIgcXVhbGl0eSBzaXRlLCBBSVJTSVMgKHBhc3N3b3JkIHByb3RlY3RlZCksIHRoZSBXZXN0ZXJuIFJlZ2lvbmFsIENsaW1hdGUgQ2VudGVyIChXUkNDKSBhbmQgdGhlIG9wZW4gc291cmNlIHNpdGUgW09wZW5BUV0oaHR0cHM6Ly9vcGVuYXEub3JnLyMvKS4gVEhlICJwYXRfZXh0ZXJuYWxGaXQiIGZ1bmN0aW9uIGNyZWF0ZXMgYSB0aW1lIHNlcmllcyBhbmQgbGluZWFyIHJlZ3Jlc3Npb24gcGxvdCBvZiB0aGUgcmVndWxhdG9yeSBkYXRhIHZzLiB0aGUgUHVycGxlQWlyIGRhdGEgYXQgYSAxIGhvdXIgdGltZSByZXNvbHV0aW9uLiBCZWxvdyBpcyBhbiBleGFtcGxlIGJ1aWx0LWluIHZhbGlkYXRpb24gb2YgdGhlICJCdXJuYWJ5MSIgc2Vuc29yLg0KDQpgYGB7ciB2YWxpZGF0aW9uX3Bsb3QsIGZpZy5oZWlnaHQ9MTJ9DQp2YWxpZGF0aW9uIDwtIHBhdF9leHRlcm5hbEZpdChwYXRfQnVybmFieTEsIHNob3dQbG90ID0gVFJVRSkNCmBgYA0KDQojIyBFeHRyYWN0IGRhdGEgZm9yIFBhcnQgMi4NCg0KV2hpbGUgdGhlIGJ1aWx0LWluIHZhbGlkYXRpb24gZnJvbSBBaXJTZW5zb3IgaXMgYSBnb29kIHN0YXJ0LCB3ZSBhcmUgaW50ZXJlc3RlZCBpbiBkaWdnaW5nIGRlZXBlciBpbnRvIHRoZSBkYXRhIGFuZCBpdHMgcGVyZm9ybWFuY2UuIEJlZm9yZSB3ZSBjYW4gZG8gdGhpcywgd2UgbmVlZCB0byBzdG9yZSB0aGUgUmVndWxhdG9yeSBNb25pdG9yaW5nIGRhdGEgKHNob3J0IGZvcm06IEZSTS9GRU0gZGF0YTsgRlJNID0gRmVkZXJhbCBSZWZlcmVuY2UgTWV0aG9kLCBGRU0gPSBGZWRlcmFsIEVxdWl2YWxlbnQgTWV0aG9kKS4gV2Ugd2lsbCBleHRyYWN0IHRoZSBmb2xsb3dpbmc6DQoNCiogMSBoIGF2ZXJhZ2UgZGF0YSBmcm9tIGFsbCB0aHJlZSBQdXJwbGVBaXIgbW9uaXRvcnMgKCJCdXJuYWJ5MSIsICJCdXJuYWJ5MiIsICJCdXJuYWJ5MyIpDQoqIDEgaCBhdmVyYWdlIGZyb20gdGhlIGNsb3Nlc3QgcmVndWxhdG9yeSBtb25pdG9yICh0aGUgTWV0cm8gVmFuY291dmVyIEJ1cm5hYnkgU291dGggc3RhdGlvbikNCg0KKiAyNCBoIGF2ZXJhZ2UgZGF0YSBmcm9tIGFsbCB0aHJlZSBQdXJwbGVBaXIgbW9uaXRvcnMgKCJCdXJuYWJ5MSIsICJCdXJuYWJ5MiIsICJCdXJuYWJ5MyIpDQoqIDI0IGggYXZlcmFnZSBkYXRhIGZyb20gdGhlIGNsb3Nlc3QgcmVndWxhdG9yeSBtb25pdG9yICh0aGUgTWV0cm8gVmFuY291dmVyIEJ1cm5hYnkgU291dGggc3RhdGlvbikNCg0KVGhlcmUgYXJlIGEgZmV3IGRhdGEgY2xlYW5pbmcgYW5kIG9yZ2FuemluZyBzdGVwcywgYnV0IHRoZSBvdXRwdXQgaXMgdHdvIGRhdGEgZnJhbWVzOg0KDQoxLiAiZGZfMWgiOiBhIGRhdGEgZnJhbWUgd2l0aCBhbGwgb2YgdGhlIDEgaCBhdmVyYWdlZCBkYXRhDQoyLiAiZGZfMjRoIjogYSBkYXRhIGZyYW1lIHdpdGggYWxsIG9mIHRoZSAyNCBob3VyIGF2ZXJhZ2VkIGRhdGENCg0KTGV0J3Mgc3RhcnQgd2l0aCB0aGUgaG91cmx5IGRhdGE6DQoNCmBgYHtyIGRhdGFfaG91cmx5fQ0KDQojIEdldCBhbGwgUHVycGxlIEFpciBTZW5zb3IgZnJvbSBCdXJuYWJ5IFNvdXRoICMgMQ0KaG91cmx5RGF0YTEgPC0gDQogIHBhdF9CdXJuYWJ5MSAlPiUNCiAgcGF0X2FnZ3JlZ2F0ZSgpICU+JQ0KICBwYXRfZXh0cmFjdERhdGEoKQ0KDQojIEdldCBhbGwgUHVycGxlIEFpciBTZW5zb3IgZnJvbSBCdXJuYWJ5IFNvdXRoICMgMg0KaG91cmx5RGF0YTIgPC0gDQogIHBhdF9CdXJuYWJ5MiAlPiUNCiAgcGF0X2FnZ3JlZ2F0ZSgpICU+JQ0KICBwYXRfZXh0cmFjdERhdGEoKQ0KDQojIEdldCBhbGwgUHVycGxlIEFpciBTZW5zb3IgZnJvbSBHaWxsZXkgQXZlICgxIGttIEVhc3QpDQpob3VybHlEYXRhMyA8LSANCiAgcGF0X0J1cm5hYnkzICU+JQ0KICBwYXRfYWdncmVnYXRlKCkgJT4lDQogIHBhdF9leHRyYWN0RGF0YSgpDQoNCiMgQWRkIEFCIGNoYW5uZWwgKGhvdyBQdXJwbGVBaXIgcmVwb3J0cyBjb25jZW50cmF0aW9ucykgYW5kIFB1bGwgUmVsZXZhbnQgRGF0YSBmb3IgQW5hbHlzaXMNCmhvdXJseURhdGExIDwtIG11dGF0ZShob3VybHlEYXRhMSxwbTI1X0FCID0gKHBtMjVfQSArIHBtMjVfQikvMikNCmRhdGFfMWhfQnVybmFieTEgPC0gcHVsbChob3VybHlEYXRhMSxwbTI1X0FCKQ0KDQoNCmhvdXJseURhdGEyIDwtIG11dGF0ZShob3VybHlEYXRhMixwbTI1X0FCID0gKHBtMjVfQSArIHBtMjVfQikvMikNCmRhdGFfMWhfQnVybmFieTIgPC0gcHVsbChob3VybHlEYXRhMixwbTI1X0FCKQ0KDQpob3VybHlEYXRhMyA8LSBtdXRhdGUoaG91cmx5RGF0YTMscG0yNV9BQiA9IChwbTI1X0EgKyBwbTI1X0IpLzIpDQpkYXRhXzFoX0J1cm5hYnkzIDwtIHB1bGwoaG91cmx5RGF0YTMscG0yNV9BQikNCg0KDQojIEdldCB0aGUgUFdGU0wgbW9uaXRvciBkYXRhIChSZWd1bGF0b3J5IERhdGEpDQoNCm1vbml0b3JJRCA8LSBwYXRfQnVybmFieTEkbWV0YSRwd2ZzbF9jbG9zZXN0TW9uaXRvcklEDQp0bGltIDwtIHJhbmdlKGhvdXJseURhdGExJGRhdGV0aW1lKQ0KDQpwd2ZzbF9tb25pdG9yIDwtDQogICAgUFdGU0xTbW9rZTo6bW9uaXRvcl9sb2FkKHRsaW1bMV0sIHRsaW1bMl0sIG1vbml0b3JJRHMgPSBtb25pdG9ySUQpICU+JQ0KICAgIFBXRlNMU21va2U6Om1vbml0b3Jfc3Vic2V0KHRsaW0gPSB0bGltKQ0KDQpwd2ZzbF9kYXRhXzFoIDwtDQogICAgcHdmc2xfbW9uaXRvciAlPiUNCiAgICBQV0ZTTFNtb2tlOjptb25pdG9yX2V4dHJhY3REYXRhKCkNCg0KbmFtZXMocHdmc2xfZGF0YV8xaCkgPC0gYygiZGF0ZSIsICJwd2ZzbF9wbTI1IikNCmRhdGFfMWhfRkVNIDwtIHB1bGwocHdmc2xfZGF0YV8xaCxwd2ZzbF9wbTI1KQ0KZGF0ZXRpbWVfMWggPC0gcHVsbChwd2ZzbF9kYXRhXzFoLGRhdGUpDQoNCg0KIyBDb21iaW5lIGFsbCBkYXRhIGludG8gYSBkYXRhIGZyYW1lDQoNCmRmXzFoIDwtIHRpYmJsZShkYXRldGltZV8xaCxkYXRhXzFoX0ZFTSxkYXRhXzFoX0J1cm5hYnkxLGRhdGFfMWhfQnVybmFieTIsZGF0YV8xaF9CdXJuYWJ5MykNCmRmXzFoIDwtIGdhdGhlcihkZl8xaCxMb2NhdGlvbixkYXRhXzFoLC1kYXRldGltZV8xaCkNCmRmXzFoJExvY2F0aW9uID0gZGZfMWgkTG9jYXRpb24gJT4lIGZjdF9yZWxhYmVsKHN0cl9yZXBsYWNlLCAiZGF0YV8xaF8iLCAiIikNCg0KYGBgDQoNCk5vdyB3ZSB3aWxsIG9yZ2FuaXplIHRoZSAyNCBoIGF2ZXJhZ2VkIGRhdGE6DQoNCmBgYHtyIGRhdGFfZGFpbHl9DQoNCiMgR2V0IGFsbCBQdXJwbGUgQWlyIFNlbnNvciBmcm9tIEJ1cm5hYnkgU291dGggIyAxDQpkYWlseURhdGExIDwtIA0KICBwYXRfQnVybmFieTEgJT4lDQogIHBhdF9hZ2dyZWdhdGUodW5pdD0iaG91cnMiLGNvdW50PTI0KSAlPiUNCiAgcGF0X2V4dHJhY3REYXRhKCkNCg0KIyBHZXQgYWxsIFB1cnBsZSBBaXIgU2Vuc29yIGZyb20gQnVybmFieSBTb3V0aCAjIDINCmRhaWx5RGF0YTIgPC0gDQogIHBhdF9CdXJuYWJ5MiAlPiUNCiAgcGF0X2FnZ3JlZ2F0ZSh1bml0PSJob3VycyIsY291bnQ9MjQpICU+JQ0KICBwYXRfZXh0cmFjdERhdGEoKQ0KDQojIEdldCBhbGwgUHVycGxlIEFpciBTZW5zb3IgZnJvbSBHaWxsZXkgQXZlICgxIGttIEVhc3QpDQpkYWlseURhdGEzIDwtIA0KICBwYXRfQnVybmFieTMgJT4lDQogIHBhdF9hZ2dyZWdhdGUodW5pdD0iaG91cnMiLGNvdW50PTI0KSAlPiUNCiAgcGF0X2V4dHJhY3REYXRhKCkNCg0KIyBBZGQgQUIgY2hhbm5lbCAoaG93IFB1cnBsZUFpciByZXBvcnRzIGNvbmNlbnRyYXRpb25zKSBhbmQgUHVsbCBSZWxldmFudCBEYXRhIGZvciBBbmFseXNpcw0KZGFpbHlEYXRhMSA8LSBtdXRhdGUoZGFpbHlEYXRhMSxwbTI1X0FCID0gKHBtMjVfQSArIHBtMjVfQikvMikNCmRhdGFfMjRoX0J1cm5hYnkxIDwtIHB1bGwoZGFpbHlEYXRhMSxwbTI1X0FCKQ0KDQoNCmRhaWx5RGF0YTIgPC0gbXV0YXRlKGRhaWx5RGF0YTIscG0yNV9BQiA9IChwbTI1X0EgKyBwbTI1X0IpLzIpDQpkYXRhXzI0aF9CdXJuYWJ5MiA8LSBwdWxsKGRhaWx5RGF0YTIscG0yNV9BQikNCg0KZGFpbHlEYXRhMyA8LSBtdXRhdGUoZGFpbHlEYXRhMyxwbTI1X0FCID0gKHBtMjVfQSArIHBtMjVfQikvMikNCmRhdGFfMjRoX0J1cm5hYnkzIDwtIHB1bGwoZGFpbHlEYXRhMyxwbTI1X0FCKQ0KDQoNCiMgR2V0IHRoZSBQV0ZTTCBtb25pdG9yIGRhdGEgKFJlZ3VsYXRvcnkgRGF0YSkNCm1vbml0b3JJRCA8LSBwYXRfQnVybmFieTEkbWV0YSRwd2ZzbF9jbG9zZXN0TW9uaXRvcklEDQp0bGltIDwtIHJhbmdlKGRhaWx5RGF0YTEkZGF0ZXRpbWUpDQoNCnB3ZnNsX21vbml0b3IgPC0NCiAgICBQV0ZTTFNtb2tlOjptb25pdG9yX2xvYWQodGxpbVsxXSwgdGxpbVsyXSwgbW9uaXRvcklEcyA9IG1vbml0b3JJRCkgJT4lDQogICAgUFdGU0xTbW9rZTo6bW9uaXRvcl9zdWJzZXQodGxpbSA9IHRsaW0pDQoNCnB3ZnNsX2RhdGFfMjRoIDwtDQogICAgcHdmc2xfbW9uaXRvciAlPiUNCiAgICBQV0ZTTFNtb2tlOjptb25pdG9yX2V4dHJhY3REYXRhKCkNCg0KbmFtZXMocHdmc2xfZGF0YV8yNGgpIDwtIGMoImRhdGUiLCAicHdmc2xfcG0yNSIpDQpwd2ZzbF9kYXRhXzI0aCA8LSB0aW1lQXZlcmFnZShwd2ZzbF9kYXRhXzI0aCxwZXJpb2Q9IjEgZGF5IikNCg0KZGF0YV8yNGhfRkVNIDwtIHB1bGwocHdmc2xfZGF0YV8yNGgscHdmc2xfcG0yNSkNCmRhdGV0aW1lXzI0aCA8LSBwdWxsKHB3ZnNsX2RhdGFfMjRoLGRhdGUpDQoNCg0KIyBDb21iaW5lIGFsbCBkYXRhIGludG8gYSBkYXRhIGZyYW1lDQoNCmRmXzI0aCA8LSB0aWJibGUoZGF0ZXRpbWVfMjRoLGRhdGFfMjRoX0ZFTSxkYXRhXzI0aF9CdXJuYWJ5MSxkYXRhXzI0aF9CdXJuYWJ5MixkYXRhXzI0aF9CdXJuYWJ5MykNCmRmXzI0aCA8LSBnYXRoZXIoZGZfMjRoLExvY2F0aW9uLGRhdGFfMjRoLC1kYXRldGltZV8yNGgpDQpkZl8yNGgkTG9jYXRpb24gPSBkZl8yNGgkTG9jYXRpb24gJT4lIGZjdF9yZWxhYmVsKHN0cl9yZXBsYWNlLCAiZGF0YV8yNGhfIiwgIiIpDQoNCmBgYA0KDQpOb3cgd2UgaGF2ZSBldmVyeXRoaW5nIHdlIG5lZWQgdG8gcHJvY2VlZCB0byBQYXJ0IDIgb2YgdGhpcyB0dXRvcmlhbCAoYXNzZXNzaW5nIHNlbnNvciBwZXJmb3JtYW5jZSBhY2NvcmRpbmcgdG8gbmV3IFUuUy4gRVBBIEJhc2UgVGVzdGluZyBQcm90b2NvbHMpLg0KDQojIFBhcnQgMjogVS5TLiBFUEEgQmFzZSBUZXN0aW5nIE1ldHJpY3MNCg0KV2hlbiB0aGUgVS5TLiBFUEEgcmVsZWFzZWQgdGhlaXIgcGVyZm9ybWFuY2UgdGVzdGluZyBwcm90b2NvbHMgZm9yIGZpbmUgcGFydGljdWxhdGUgbWF0dGVyIHNlbnNvcnMsIHRoZXkgYWxzbyByZWxlYXNlZCBhIFsiRmlsbGFibGUgUmVwb3J0aW5nIFRlbXBsYXRlIl0oaHR0cHM6Ly9jZnB1Yi5lcGEuZ292L3NpL3NpX3B1YmxpY19maWxlX2Rvd25sb2FkLmNmbT9wX2Rvd25sb2FkX2lkPTU0MjEwNCZMYWI9Q0VNTSkuIFRoZSBhaW0gb2YgdGhlIFUuUy4gRVBBIHJlcG9ydCB3YXMgYW4gYXR0ZW1wdCB0byBzdGFuZGFyZGl6ZSB0aGUgd2F5IHNlbnNvciBwZXJmb3JtYW5jZSBpcyByZXBvcnRlZC4gVG8gdGhhdCBlbmQsIHRoaXMgcGFydCBvZiB0aGUgdHV0b3JpYWwgYnVpbGRzIGFsbCBvZiB0aGUgcmVsZXZhbnQgcGxvdHMgYW5kIHRhYmxlcyByZXF1aXJlZCBpbiB0aGUgRmlsbGFibGUgUmVwb3J0LiBXZSB3aWxsIHVzZSBvdXIgdGhyZWUgQnVybmFieSBTb3V0aCBQdXJwbGVBaXIgbW9uaXRvcnMgZm9yIHRoaXMgcHVycG9zZS4NCg0KVGhpcyBhbmFseXNpcyBpcyBub3QgZXh0ZW5zaXZlOyB0aGVyZSBhcmUgbnVtZXJvdXMgZGV0YWlscyBhbmQgY2F2ZWF0cyBtZW50aW9uZWQgd2l0aGluIHRoZSBmdWxsIFUuUy4gRVBBIHJlcG9ydCBbQER1dmFsbDIwMjFdLiBJdCBpcyBzdHJvbmdseSByZWNvbW1lbmRlZCB0aGF0IGluIGFkZGl0aW9uIHRvIHRoaXMgdHV0b3JpYWwsIGFueSBsb3ctY29zdCBQTSBzZW5zb3IgdXNlcnMgcmVhZCB0aGUgVS5TLiBFUEEgcmVwb3J0IGluIGZ1bGwuIEFkZGl0aW9uYWxseSwgdGhpcyB0dXRvcmlhbCBpcyBvbmx5IGFwcGxpY2FibGUgdG8gIkJhc2UgVGVzdGluZyIgKGkuZS4sIGZpZWxkIHRlc3RzKS4gVGhlIFUuUy4gRVBBIGFsc28gcmVjb21tZW5kcyAiRW5oYW5jZWQgVGVzdGluZyIgYmFzZWQgb24gbGFiLWJhc2VkIGFuYWx5c2VzLiBUaGlzIGlzIG5vdCBjb3ZlcmVkIGhlcmUuICANCg0KVGhpcyB0dXRvcmlhbCBjb3ZlcnMgdGhlIGZvbGxvd2luZyBlbGVtZW50cyBvZiB0aGUgRmlsbGFibGUgUmVwb3J0IGZvciBCYXNlIFRlc3Rpbmc6DQoNCiogVGltZSBTZXJpZXMgUGxvdHMNCiogU2NhdHRlciBQbG90czogQ29tcGFyaXNvbiB0byBGUk0vRkVNDQoqIFBlcmZvcm1hbmNlIE1ldHJpY3MgDQogICsgU2Vuc29yLUZSTS9GRU0gQWNjdXJhY3kNCiAgKyBTZW5zb3ItU2Vuc29yIFByZWNpc2lvbg0KKiBNZXRlb3JvbG9naWNhbCBDb25kaXRpb25zIER1cmluZyBEZXBsb3ltZW50DQoqIE1ldGVvcm9sb2dpY2FsIEluZmx1ZW5jZQ0KKiBJbmRpdmlkdWFsIFNlbnNvci1GUk0vRkVNIFNjYXR0ZXIgUGxvdHMNCiogVGFidWxhciBTdGF0aXN0aWNzDQoNCg0KIyMgVGltZSBTZXJpZXMgUGxvdHMNCg0KSGVyZSB3ZSBwbG90IHRpbWUgc2VyaWVzIGZvciBhbGwgdGhyZWUgUHVycGxlQWlyIG1vbml0b3JzIGFuZCB0aGUgRlJNL0ZFTSBtb25pdG9yIGZvciBib3RoIDEgaCBhbmQgMjQgaCBhdmVyYWdlIFBNfjIuNX4gY29uY2VudHJhdGlvbnMuDQoNCmBgYCB7ciB0aW1lX3NlcmllcywgZmlnLnNob3c9ImhvbGQiLCBvdXQud2lkdGg9IjUwJSJ9DQoNCmdncGxvdChkZl8xaCxhZXMoeD1kYXRldGltZV8xaCx5PWRhdGFfMWgsZ3JvdXA9TG9jYXRpb24sY29sb3I9TG9jYXRpb24pKSArDQogIGdlb21fbGluZShhbHBoYT0wLjcsc2l6ZT0xKSArDQogIHhsYWIoIkRhdGUiKSArDQogIHlsYWIoZXhwcmVzc2lvbihQTVsyLjVdfkNvbmNlbnRyYXRpb25+KMK1Zy9tXjMpKSkgKw0KICB5bGltKDAsMjAwKSArDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKSArDQogIGdndGl0bGUoIjEgaCBBdmVyYWdlIikNCg0KZ2dwbG90KGRmXzI0aCxhZXMoeD1kYXRldGltZV8yNGgseT1kYXRhXzI0aCxncm91cD1Mb2NhdGlvbixjb2xvcj1Mb2NhdGlvbikpICsNCiAgZ2VvbV9saW5lKGFscGhhPTAuNyxzaXplPTEpICsgDQogIHhsYWIoIkRhdGUiKSArDQogIHlsYWIoZXhwcmVzc2lvbihQTVsyLjVdfkNvbmNlbnRyYXRpb25+KMK1Zy9tXjMpKSkgKw0KICB5bGltKDAsMjAwKSArDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKSArDQogIGdndGl0bGUoIjI0IGggQXZlcmFnZSIpDQoNCmBgYA0KRnJvbSB0aGUgdGltZSBzZXJpZXMgcGxvdHMsIHdlIGNsZWFybHkgc2VlIGEgcGVyaW9kIHdoZXJlIHRoZSBQdXJwbGVBaXIgc2Vuc29ycyBhcmUgb3ZlcmVzdGltYXRpbmcgUE1+Mi41fiBjb25jZW50cmF0aW9ucyAoZWFybHkgT2N0b2JlcikuIFRoZSBzY2FsZSBvbiB0aGUgeS1heGlzIGlzIGFsc28gaW5mbHVlbmNlZCBieSBhIHdpbGRmaXJlIHNtb2tlIGVwaXNvZGUgd2hpY2ggb2NjdXJyZWQgaW4gbWlkLVNlcHRlbWJlciAyMDIwLg0KDQojIyBTY2F0dGVyIFBsb3RzOiBDb21wYXJpc29uIHRvIEZSTS9GRU0NCg0KSGVyZSB3ZSBzY2F0dGVyIHBsb3RzIG9mIGFsbCB0aHJlZSBQdXJwbGVBaXIgbW9uaXRvcnMgdnMuIHRoZSBGUk0vRkVNIG1vbml0b3IgZm9yIGJvdGggMSBoIGFuZCAyNCBoIGF2ZXJhZ2UgUE1+Mi41fiBjb25jZW50cmF0aW9ucy4gVGhlcmUgaXMgYWxzbyBhIDE6MSBsaW5lIGFkZGVkIHRvIHF1aWNrbHkgYXNzZXNzIHdoZXRoZXIgdGhlIFB1cnBsZUFpciBtb25pdG9ycyBhcmUgb3Zlci0gb3IgdW5kZXItZXN0aW1hdGluZy4NCg0KYGBgIHtyIHNjYXR0ZXJfcGxvdCxmaWcuc2hvdz0iaG9sZCIsIG91dC53aWR0aD0iNTAlIn0NCmRmXzFoX3NjYXR0ZXIgPC0gc3ByZWFkKGRmXzFoLExvY2F0aW9uLGRhdGFfMWgpDQpkZl8xaF9zY2F0dGVyIDwtIGdhdGhlcihkZl8xaF9zY2F0dGVyLExvY2F0aW9uLGRhdGFfMWgsLWRhdGV0aW1lXzFoLC1GRU0pDQoNCmdncGxvdChkZl8xaF9zY2F0dGVyLGFlcyh4PUZFTSx5PWRhdGFfMWgsZ3JvdXA9TG9jYXRpb24sY29sb3I9TG9jYXRpb24pKSArDQogIGdlb21fcG9pbnQoYWxwaGE9MC43LHNpemU9MSkgKw0KICB4bGFiKGV4cHJlc3Npb24oRkVNflBNWzIuNV1+KMK1Zy9tXjMpKSkgKw0KICB5bGFiKGV4cHJlc3Npb24oU2Vuc29yflBNWzIuNV1+KMK1Zy9tXjMpKSkgKw0KICBnZ3RpdGxlKCIxIGggQXZlcmFnZSIpICsNCiAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSksIHRleHQgPSBlbGVtZW50X3RleHQoc2l6ZT0yMCkpICsNCiAgeGxpbSgwLDMwKSArDQogIHlsaW0oMCwzMCkgKw0KICBnZW9tX2FibGluZShzbG9wZT0xLGxpbmV0eXBlPSJkYXNoZWQiKSArDQogIGNvb3JkX2VxdWFsKCkNCg0KZGZfMjRoX3NjYXR0ZXIgPC0gc3ByZWFkKGRmXzI0aCxMb2NhdGlvbixkYXRhXzI0aCkNCmRmXzI0aF9zY2F0dGVyIDwtIGdhdGhlcihkZl8yNGhfc2NhdHRlcixMb2NhdGlvbixkYXRhXzI0aCwtZGF0ZXRpbWVfMjRoLC1GRU0pDQoNCmdncGxvdChkZl8yNGhfc2NhdHRlcixhZXMoeD1GRU0seT1kYXRhXzI0aCxncm91cD1Mb2NhdGlvbixjb2xvcj1Mb2NhdGlvbikpICsNCiAgZ2VvbV9wb2ludChhbHBoYT0wLjcsc2l6ZT0yKSArDQogIHhsYWIoZXhwcmVzc2lvbihGRU1+UE1bMi41XX4owrVnL21eMykpKSArDQogIHlsYWIoZXhwcmVzc2lvbihTZW5zb3J+UE1bMi41XX4owrVnL21eMykpKSArDQogIGdndGl0bGUoIjI0IGggQXZlcmFnZSIpICsNCiAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSksIHRleHQgPSBlbGVtZW50X3RleHQoc2l6ZT0yMCkpICsNCiAgeGxpbSgwLDMwKSArDQogIHlsaW0oMCwzMCkgKw0KICBnZW9tX2FibGluZShzbG9wZT0xLGxpbmV0eXBlPSJkYXNoZWQiKSArDQogIGNvb3JkX2VxdWFsKCkNCg0KYGBgDQpGcm9tIHRoZXNlIHNjYXR0ZXIgcGxvdHMgem9vbWVkIGluIHRvIDMwIM68Zy9tXjNeICwgd2UgY2FuIHNlZSB0aGF0IHRoZSBQdXJwbGVBaXIgbW9uaXRvcnMgdGVuZCB0byBzbGlnaHRseSBvdmVyZXN0aW1hdGUgY29uY2VudHJhdGlvbnMgKGRhdGEgZmFsbHMgYWJvdmUgMToxIGxpbmUpLiBXZSB3aWxsIGV4cGxpY2l0bHkgY2FsY3VsYXRlIHRoZSBzbG9wZSBpbiB0aGUgIlBlcmZvcm1hbmNlIE1ldHJpY3MiIHNlY3Rpb24uIA0KDQojIyBQZXJmb3JtYW5jZSBNZXRyaWNzDQoNClRoZSBVLlMuIEVQQSBQTX4yLjV+IFRlc3RpbmcgUmVwb3J0IHJlY29tbWVuZHMgcmVwb3J0aW5nIG9uIHRoZSBmb2xsb3dpbmcgbWV0cmljczoNCg0KKiBTZW5zb3IgQWNjdXJhY3kgTWV0cmljcw0KICArIENvZWZmaWNpZW50IG9mIERldGVybWluYXRpb24gKFJeMl4pDQogICsgU2xvcGUNCiAgKyBJbnRlcmNlcHQNCiAgKyBSb290IE1lYW4gU3F1YXJlIEVycm9yIChSTVNFKQ0KICArIE5vcm1hbGl6ZWQgUm9vdCBNZWFuIFNxdWFyZSBFcnJvciAoTlJNU0UpDQoqIFNlbnNvciBQcmVjaXNpb24gTWV0cmljcw0KICArIFN0YW5kYXJkIERldmlhdGlvbiAoU0QpDQogICsgQ29lZmZpY2llbnQgb2YgVmFyaWF0aW9uIChDVikNCiAgDQpUaGVzZSBhcmUgZmlyc3QgZGVmaW5lZCBhbmQgdGhlbiBjYWxjdWxhdGVkIGluIHRoZSByZXNwZWN0aXZlIHN1Yi1zZWN0aW9ucy4gRm9yIGFsbCBlcXVhdGlvbnMgaW4gdGhpcyBzZWN0aW9uIHdlIHdpbGwgYXNzdW1lIHRoYXQgeCBpcyB0aGUgRkVNL0ZSTSBkYXRhIGFuZCB5IGlzIHRoZSBzZW5zb3IgZGF0YS4NCg0KKipDb2VmZmljaWVudCBvZiBEZXRlcm1pbmF0aW9uIChSXjJeKSoqDQoNClRoZSBjb2VmZmljaWVudCBvZiBkZXRlcm1pbmF0aW9uIGlzIGEgbWV0cmljIHRoYXQgYXNzZXNzIGhvdyBkaWZmZXJlbmNlcyBpbiBvbmUgdmFyaWFibGUgY2FuIGJlIGV4cGxhaW5lZCBieSB0aGUgZGlmZmVyZW5jZSBpbiBhIHNlY29uZCB2YXJpYWJsZS4gDQoNCiQkUl4yID0gMS1cZnJhY3tcc3VtKHlfaSAteF9pKV4yfXtcc3VtKHlfaS1cYmFye3l9KV4yfSQkDQoqKlNsb3BlIChtKSoqDQoNClNsb3BlIGlzIGNhbGN1bGF0ZWQgYnkgZmluZGluZyB0aGUgcmF0aW8gb2YgdGhlICJ2ZXJ0aWNhbCBjaGFuZ2UiIChyaXNlKSB0byB0aGUgImhvcml6b250YWwgY2hhbmdlIiAocnVuKSBiZXR3ZWVuIChhbnkpIHR3byBkaXN0aW5jdCBwb2ludHMgb24gYSBsaW5lLiBGb3IgYSBzaW1wbGUgbGluZWFyIHJlZ3Jlc3Npb24gYmV0d2VlbiB0aGUgRkVNL0ZSTSBkYXRhICh4KSBhbmQgdGhlIHNlbnNvciBkYXRhICh5KSwgaXQgaXMgY2FsY3VsYXRlZCBhczogDQoNCiQkbT1cZnJhY3tcc3VtKHlfaS1cYmFye3l9KVx0aW1lcyh4X2ktXGJhcnt4fSl9e1xzdW0oeF9pLVxiYXJ7eH0pXjJ9JCQNCioqSW50ZXJjZXB0IChiKSoqDQoNClRoaXMgaXMgdGhlIHZhbHVlIGF0IHdoaWNoIHRoZSBmaXR0ZWQgbGluZSBmcm9tIGEgc2ltcGxlIGxpbmVhciByZWdyZXNzaW9uIGNyb3NzZXMgdGhlIHktYXhpcy4gRm9yIGEgc2ltcGxlIGxpbmVhciByZWdyZXNzaW9uIGJldHdlZW4gdGhlIEZFTS9GUk0gZGF0YSAoeCkgYW5kIHRoZSBzZW5zb3IgZGF0YSAoeSksIGl0IGlzIGNhbGN1bGF0ZWQgYXM6IA0KDQokJCBiID0gXGJhcnt5fSAtIG1cdGltZXNcYmFye3h9JCQNCg0KKipSb290IE1lYW4gU3F1YXJlIEVycm9yIChSTVNFKSoqDQoNClRoZSBSTVNFIGlzIG1lYXN1cmUgb2YgdGhlIGRpZmZlcmVuY2VzIGJldHdlZW4gdGhlIHNlbnNvciBkYXRhICh5KSBhbmQgdGhlIEZFTS9GUk0gbW9uaXRvciBkYXRhICh4KS4NCg0KJCRSTVNFPVxzcXJ0e1xmcmFjezF9e059XHN1bV97aT0xfV57Tn0oeV9pIC14X2kpXjJ9JCQNCg0KSWYgeW91IHdpc2ggdG8gY2FsY3VsYXRlIG9uZSBSTVNFIGFjcm9zcyBtdWx0aXBsZSAoTSkgc2Vuc29ycywgdGhlIGZvbGxvd2luZyBhZGp1c3RlZCBmb3JtdWxhIGlzIHVzZWQ6DQoNCiQkUk1TRT1cc3FydHtcZnJhY3sxfXtOXHRpbWVzIE19XHN1bV97aj0xfV57TX1cc3VtX3tpPTF9XntOfSh5X3tpan0gLXhfaSleMn0kJA0KKipOb3JtYWxpemVkIFJvb3QgTWVhbiBTcXVhcmUgRXJyb3IgKE5STVNFKSoqDQoNClRoZSBOUk1TRSBoZWxwcyBhY2NvdW50IGZvciBwZXJpb2RzIHdoZW4gYW1iaWVudCBjb25jZW50cmF0aW9ucyBhcmUgaGlnaCAod2hpY2ggbWF5IHNrZXcgdGhlIFJNU0UpLiBJdCBpcyBjYWxjdWxhdGVkIGJ5IG5vcm1hbGl6aW5nIHRoZSBSTVNFIGJ5IHRoZSBhdmVyYWdlIGNvbmNlbnRyYXRpb24gbWVhc3VyZWQgYnkgdGhlIEZFTS9GUk0gbW9uaXRvci4NCg0KJCROUk1TRSA9IFxmcmFje1JNU0V9e1xiYXJ7eH19XHRpbWVzMTAwJCQNCg0KKipTdGFuZGFyZCBEZXZpYXRpb24qKg0KDQpUaGlzIGlzIHRoZSBzdGFuZGFyZCBkZXZpYXRpb24gb2YgdGhlIHNlbnNvciBQTV8yLjVfIGNvbmNlbnRyYXRpb24gbWVhc3VyZW1lbnRzLiBJdCBpcyBjYWxjdWxhdGVkIHVzaW5nIGRhdGEgZnJvbSBhbGwgY28tZGVwbG95ZWQgbG93LWNvc3Qgc2Vuc29ycy4gSXQgaXMgZGVmaW5lZCBhcyBmb2xsb3dzOg0KDQokJFNEPVxzcXJ0e1xmcmFjezF9eyhOXHRpbWVzIE0pLTF9XHN1bV97aj0xfV57TX1cc3VtX3tkPTF9XntOfSh5X3tkan0gLVxiYXJ7eV9kfSleMn0kJA0KDQpXaGVyZSBNIGlzIHRoZSBudW1iZXIgb2YgaWRlbnRpY2FsIHNlbnNvcnMgb3BlcmF0ZWQgc2ltdWx0YW5lb3VzbHksIE4gaXMgdGhlIG51bWJlciBvZiBwZXJpb2RzIGR1cmluZyB3aGljaCBhbGwgaW5zdHJ1bWVudHMgYXJlIG9wZXJhdGluZyBhbmQgcmVwb3J0aW5nIHZhbGlkIGRhdGEsIHl+ZGp+IGlzIHRoZSBhdmVyYWdlIGNvbmNlbnRyYXRpb24gZm9yIGRheSBvciBob3VyIF9kXyBhbmQgc2Vuc29yIF9qXywgYW5kICRcYmFye3lfZH0kIGlzIHRoZSBhdmVyYWdlIGNvbmNlbnRyYXRpb24gYWNyb3NzIGFsbCBzZW5zb3JzIGZvciBkYXkgb3IgaG91ciBfZF8uDQoNCioqQ29lZmZpY2llbnQgb2YgVmFyaWF0aW9uIChDVikqKg0KDQpUaGlzIGlzIHRoZSByZWxhdGl2ZSBzdGFuZGFyZCBkZXZpYXRpb24gKFNEKSBkaXZpZGVkIGJ5IHRoZSBkZXBsb3ltZW50IGF2ZXJhZ2VkIHNlbnNvciBQTX4yLjV+IGNvbmNlbnRyYXRpb24gYWNyb3NzIHRoZSBmaWVsZCB0ZXN0LiBJdCBpcyBkZWZpbmVkIGFzIGZvbGxvd3M6DQoNCiQkQ1Y9IFxmcmFje1NEfXtcYmFye3l9fVx0aW1lcyAxMDAkJA0KV2hlcmUgJFxiYXJ7eX0kIGlzIHRoZSBkZXBsb3ltZW50IGF2ZXJhZ2VkIHNlbnNvciBQTV8yLjVfIGNvbmNlbnRyYXRpb24gZm9yIHRoZSBmaWVsZCB0ZXN0Lg0KDQojIyMgU2Vuc29yLUZSTS9GRU0gQWNjdXJhY3kNCg0KV2Ugd2lsbCBjYWxjdWxhdGUgYWNjdXJhY3kgbWV0cmljcyBmb3IgYm90aCBob3VybHkgYW5kIGRhaWx5IGF2ZXJhZ2luZyBwZXJpb2RzLiBXZSB3aWxsIGFsc28gcHJvdmlkZSB0YWJ1bGFyIHN0YXRpc3RpY3MsIGFzIHJlY29tbWVuZGVkLg0KV2Ugd2lsbCB1c2UgYnVpbHQtaW4gbGluZWFyIHJlZ3Jlc3Npb24gZnVuY3Rpb25zIGluIFIgdG8gY2FsY3VsYXRlIHNsb3BlLCBpbnRlcmNlcHQsIGFuZCBSXjJeLiBSTVNFIGFuZCBOUk1TRSBhcmUgY2FsY3VsYXRlZCBtYW51YWxseS4gQXQgdGhlIGVuZCBvZiB0aGlzIGNvZGUgY2h1bmsgdGhlIGRhdGEgYXJlIHJlcG9ydGVkIGluIHRhYnVsYXIgZm9ybWF0LCBmb2xsb3dlZCBieSBhIHNlY3Rpb24gcGxvdHRpbmcgdGhlIHJlc3VsdHMuIA0KDQpgYGB7ciBhY2N1cmFjeV9jYWxjdWxhdGlvbnN9DQoNCiMjIDEgaG91ciBkYXRhDQoNCiMgQWRkIGEgY29sdW1uIHRvIG91ciBkYXRhIGZyYW1lIHdpdGggdGhlIHNxdWFyZSByZXNpZHVhbC4gVGhpcyBpcyBoZWxwZnVsIGZvciBSTVNFIGNhbGN1bGF0aW9ucy4NCg0KZGZfMWhfYWNjdXJhY3kgPC0gZGZfMWhfc2NhdHRlciAlPiUNCiAgbXV0YXRlKHNxLnJlc2lkdWFsPShkYXRhXzFoIC0gRkVNKV4yKQ0KDQojIFNsb3BlLCBpbnRlcmNlcHQgYW5kIFIyLiBRdWlja2x5IGNhbGN1bGF0ZWQgdXNpbmcgUiBQYWNrYWdlICdicm9vbXMnIGFuZCB0aGVpciB0aWR5IGFuZCBnbGFuY2UgZnVuY3Rpb25zDQoNCm1vZGVsX3BhcmFtc18xaCA8LSBkZl8xaF9hY2N1cmFjeSAlPiUgbmVzdChkYXRhID0gLUxvY2F0aW9uKSAlPiUgDQogIG11dGF0ZShtb2RlbCA9IG1hcChkYXRhLCB+bG0oZGF0YV8xaCB+IEZFTSwgZGF0YSA9IC4pKSwgdGlkaWVkID0gbWFwKG1vZGVsLCB0aWR5KSkgJT4lIHVubmVzdCh0aWRpZWQpIA0KDQptb2RlbF9zdGF0c18xaCA8LSBkZl8xaF9hY2N1cmFjeSAlPiUgbmVzdChkYXRhID0gLUxvY2F0aW9uKSAlPiUgDQogIG11dGF0ZShtb2RlbCA9IG1hcChkYXRhLCB+bG0oZGF0YV8xaCB+IEZFTSwgZGF0YSA9IC4pKSwgdGlkaWVkID0gbWFwKG1vZGVsLCBnbGFuY2UpKSAlPiUgdW5uZXN0KHRpZGllZCkNCg0Kc2xvcGVzXzFoIDwtIG1vZGVsX3BhcmFtc18xaCAlPiUgZmlsdGVyKHRlcm09PSJGRU0iKSAlPiUgc2VsZWN0KExvY2F0aW9uLGVzdGltYXRlKSAlPiUgcmVuYW1lKC4sc2xvcGVfMWg9ZXN0aW1hdGUpDQppbnRlcmNlcHRzXzFoIDwtIG1vZGVsX3BhcmFtc18xaCAlPiUgZmlsdGVyKHRlcm09PSIoSW50ZXJjZXB0KSIpICU+JSBzZWxlY3QoTG9jYXRpb24sZXN0aW1hdGUpICU+JSByZW5hbWUoLixpbnRlcmNlcHRfMWg9ZXN0aW1hdGUpDQpyMl8xaCA8LSBtb2RlbF9zdGF0c18xaCAlPiUgc2VsZWN0KExvY2F0aW9uLHIuc3F1YXJlZCkgJT4lIHJlbmFtZSguLHIyXzFoPXIuc3F1YXJlZCkNCg0KIyBSTVNFIGFuZCBOUk1TRTogTWFudWFsIGNhbGN1bGF0aW9ucy4gDQoNCiMgQ2FsY3VsYXRlIFJNU0UgYW5kIE5STVNFDQpSTVNFXzFoIDwtIGRmXzFoX2FjY3VyYWN5ICU+JQ0KICBncm91cF9ieShMb2NhdGlvbikgJT4lDQogIHN1bW1hcml6ZShSTVNFXzFoPXNxcnQobWVhbihzcS5yZXNpZHVhbCxuYS5ybT1UKSksDQogICAgICAgICAgICBOUk1TRV8xaD1zcXJ0KG1lYW4oc3EucmVzaWR1YWwsbmEucm09VCkpL21lYW4oRkVNLG5hLnJtPVQpKjEwMCkNCg0KYWNjdXJhY3lfMWggPC0gIGxlZnRfam9pbihyMl8xaCwgc2xvcGVzXzFoLCBieT0nTG9jYXRpb24nKSAlPiUNCiAgICAgICAgICAgICAgICBsZWZ0X2pvaW4oLiwgaW50ZXJjZXB0c18xaCwgYnk9J0xvY2F0aW9uJykgJT4lDQogICAgICAgICAgICAgICAgbGVmdF9qb2luKC4sUk1TRV8xaCxieT0nTG9jYXRpb24nKQ0KDQojIyAyNCBob3VyIGRhdGENCg0KIyBBZGQgYSBjb2x1bW4gdG8gb3VyIGRhdGEgZnJhbWUgd2l0aCB0aGUgc3F1YXJlIHJlc2lkdWFsLiBIZWxwZnVsIGZvciBSTVNFIGNhbGN1bGF0aW9ucy4NCmRmXzI0aF9hY2N1cmFjeSA8LSBkZl8yNGhfc2NhdHRlciAlPiUNCiAgbXV0YXRlKHNxLnJlc2lkdWFsPShkYXRhXzI0aCAtIEZFTSleMikNCg0KIyBTbG9wZSwgaW50ZXJjZXB0IGFuZCBSMi4gUXVpY2tseSBjYWxjdWxhdGVkIHVzaW5nIFIgUGFja2FnZSAnYnJvb21zJyBhbmQgdGhlaXIgdGlkeSBhbmQgZ2xhbmNlIGZ1bmN0aW9ucw0KDQptb2RlbF9wYXJhbXNfMjRoIDwtIGRmXzI0aF9hY2N1cmFjeSAlPiUgbmVzdChkYXRhID0gLUxvY2F0aW9uKSAlPiUgDQogIG11dGF0ZShtb2RlbCA9IG1hcChkYXRhLCB+bG0oZGF0YV8yNGggfiBGRU0sIGRhdGEgPSAuKSksIHRpZGllZCA9IG1hcChtb2RlbCwgdGlkeSkpICU+JSB1bm5lc3QodGlkaWVkKSANCg0KbW9kZWxfc3RhdHNfMjRoIDwtIGRmXzI0aF9hY2N1cmFjeSAlPiUgbmVzdChkYXRhID0gLUxvY2F0aW9uKSAlPiUgDQogIG11dGF0ZShtb2RlbCA9IG1hcChkYXRhLCB+bG0oZGF0YV8yNGggfiBGRU0sIGRhdGEgPSAuKSksIHRpZGllZCA9IG1hcChtb2RlbCwgZ2xhbmNlKSkgJT4lIHVubmVzdCh0aWRpZWQpDQoNCnNsb3Blc18yNGggPC0gbW9kZWxfcGFyYW1zXzI0aCAlPiUgZmlsdGVyKHRlcm09PSJGRU0iKSAlPiUgc2VsZWN0KExvY2F0aW9uLGVzdGltYXRlKSAlPiUgcmVuYW1lKC4sc2xvcGVfMjRoPWVzdGltYXRlKQ0KaW50ZXJjZXB0c18yNGggPC0gbW9kZWxfcGFyYW1zXzI0aCAlPiUgZmlsdGVyKHRlcm09PSIoSW50ZXJjZXB0KSIpICU+JSBzZWxlY3QoTG9jYXRpb24sZXN0aW1hdGUpICU+JSByZW5hbWUoLixpbnRlcmNlcHRfMjRoPWVzdGltYXRlKQ0KcjJfMjRoIDwtIG1vZGVsX3N0YXRzXzI0aCAlPiUgc2VsZWN0KExvY2F0aW9uLHIuc3F1YXJlZCkgJT4lIHJlbmFtZSguLHIyXzI0aD1yLnNxdWFyZWQpDQoNCiMgUk1TRSBhbmQgTlJNU0U6IDogTWFudWFsIGNhbGN1bGF0aW9ucw0KDQpSTVNFXzI0aCA8LSBkZl8yNGhfYWNjdXJhY3kgJT4lDQogIGdyb3VwX2J5KExvY2F0aW9uKSAlPiUNCiAgc3VtbWFyaXplKFJNU0VfMjRoPXNxcnQobWVhbihzcS5yZXNpZHVhbCxuYS5ybT1UKSksDQogICAgICAgICAgICBOUk1TRV8yNGg9c3FydChtZWFuKHNxLnJlc2lkdWFsLG5hLnJtPVQpKS9tZWFuKEZFTSwgbmEucm09VCkqMTAwKQ0KDQphY2N1cmFjeV8yNGggPC0gIGxlZnRfam9pbihyMl8yNGgsIHNsb3Blc18yNGgsIGJ5PSdMb2NhdGlvbicpICU+JQ0KICAgICAgICAgICAgICAgIGxlZnRfam9pbiguLCBpbnRlcmNlcHRzXzI0aCwgYnk9J0xvY2F0aW9uJykgJT4lDQogICAgICAgICAgICAgICAgbGVmdF9qb2luKC4sUk1TRV8yNGgsYnk9J0xvY2F0aW9uJykNCg0KYWNjdXJhY3kgPC0gbGVmdF9qb2luKGFjY3VyYWN5XzFoLGFjY3VyYWN5XzI0aCxieT0nTG9jYXRpb24nKQ0KDQojIFdlIHdpbGwgdXNlIHRoaXMgZ3JvdXBlZCBkYXRhIGxhdGVyIHdoZW4gcGxvdHRpbmcNCmFjY3VyYWN5X2xvbmcgPC0gbWVsdChzZXREVChhY2N1cmFjeSksaWQudmFycz0nTG9jYXRpb24nLG1lYXN1cmUudmFycz1wYXR0ZXJucygiXnIyXyIsIl5zbG9wZV8iLCJeaW50ZXJjZXB0XyIsIl5STVNFXyIsIl5OUk1TRV8iKSx2YWx1ZS5uYW1lID0gYygicjIiLCAic2xvcGUiLCJpbnRlcmNlcHQiLCJSTVNFIiwiTlJNU0UiKSx2YXJpYWJsZS5uYW1lID0gImF2Zy50aW1lIikNCmFjY3VyYWN5X2xvbmckYXZnLnRpbWUgPC0gcmVjb2RlX2ZhY3RvcihhY2N1cmFjeV9sb25nJGF2Zy50aW1lLGAxYD0iMSBoIixgMmA9IjI0IGgiKQ0KDQoNCiMjIFByaW50IG1ldHJpY3MgYXMgdGFibGUNCg0KYWNjdXJhY3lfbWVhbiA8LSBhY2N1cmFjeSAlPiUgc3VtbWFyaXplX2lmKGlzLm51bWVyaWMsbWVhbikNCmFjY3VyYWN5IDwtIGJpbmRfcm93cyhhY2N1cmFjeSxhY2N1cmFjeV9tZWFuKQ0KYWNjdXJhY3lbNCwxXSA9ICJNZWFuIg0KDQphY2N1cmFjeSAlPiUNCiAga2JsKGNhcHRpb249IioqU2Vuc29yLUZFTSBBY2N1cmFjeSBNZXRyaWNzKioiLCBkaWdpdHM9MiwgY29sLm5hbWVzID0gYygiU2Vuc29yIElEIiwiUl4yXiIsIlNsb3BlIiwiSW50ZXJjZXB0IiwiUk1TRSIsIk5STVNFIiwiUl4yXiIsIlNsb3BlIiwiSW50ZXJjZXB0IiwiUk1TRSIsIk5STVNFIikpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKSAlPiUNCiAgYWRkX2hlYWRlcl9hYm92ZShjKCIgIiA9IDEsICIxIGgiID0gNSwgIjI0IGgiID0gNSkpDQoNCmBgYA0KDQpOb3cgd2Ugd2lsbCBwbG90IHRoZSByZXN1bHRzIHVzaW5nIGJveCBwbG90cy4gDQoNCmBgYHtyIHBsb3RfYWNjdXJhY3ksIGZpZy5zaG93PSJob2xkIiwgb3V0LndpZHRoPSI1MCUiLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KIyBQcmVwIGRhdGEgZm9yIGJveCBwbG90cw0KDQpnZ3Bsb3QoYWNjdXJhY3lfbG9uZyxhZXMoeD1hdmcudGltZSx5PXIyLGZpbGw9YXZnLnRpbWUpKSArDQogIGdlb21fYm94cGxvdChzaG93LmxlZ2VuZCA9IEYpICsNCiAgZ2VvbV9kb3RwbG90KGJpbmF4aXM9J3knLCBzdGFja2Rpcj0nY2VudGVyJywgZG90c2l6ZT0xLCBzaG93LmxlZ2VuZD1GKSArDQogIHhsYWIoIkF2ZXJhZ2luZyBUaW1lIikgKw0KICB5bGFiKGV4cHJlc3Npb24oUl4yKSkgKw0KICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSwgdGV4dCA9IGVsZW1lbnRfdGV4dChzaXplPTIwKSkNCg0KZ2dwbG90KGFjY3VyYWN5X2xvbmcsYWVzKHg9YXZnLnRpbWUseT1zbG9wZSxmaWxsPWF2Zy50aW1lKSkgKw0KICBnZW9tX2JveHBsb3Qoc2hvdy5sZWdlbmQgPSBGKSArDQogIGdlb21fZG90cGxvdChiaW5heGlzPSd5Jywgc3RhY2tkaXI9J2NlbnRlcicsIGRvdHNpemU9MSwgc2hvdy5sZWdlbmQ9RikgKw0KICB4bGFiKCJBdmVyYWdpbmcgVGltZSIpICsNCiAgeWxhYigiU2xvcGUiKSArDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKQ0KDQpnZ3Bsb3QoYWNjdXJhY3lfbG9uZyxhZXMoeD1hdmcudGltZSx5PWludGVyY2VwdCxmaWxsPWF2Zy50aW1lKSkgKw0KICBnZW9tX2JveHBsb3Qoc2hvdy5sZWdlbmQgPSBGKSArDQogIGdlb21fZG90cGxvdChiaW5heGlzPSd5Jywgc3RhY2tkaXI9J2NlbnRlcicsIGRvdHNpemU9MSwgc2hvdy5sZWdlbmQ9RikgKw0KICB4bGFiKCJBdmVyYWdpbmcgVGltZSIpICsNCiAgeWxhYihleHByZXNzaW9uKEludGVyY2VwdH4ozrxnL21eMykpKSArDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKQ0KDQpnZ3Bsb3QoYWNjdXJhY3lfbG9uZyxhZXMoeD1hdmcudGltZSx5PVJNU0UsZmlsbD1hdmcudGltZSkpICsNCiAgZ2VvbV9ib3hwbG90KHNob3cubGVnZW5kID0gRikgKw0KICBnZW9tX2RvdHBsb3QoYmluYXhpcz0neScsIHN0YWNrZGlyPSdjZW50ZXInLCBkb3RzaXplPTEsIHNob3cubGVnZW5kPUYpICsNCiAgeGxhYigiQXZlcmFnaW5nIFRpbWUiKSArDQogIHlsYWIoZXhwcmVzc2lvbihSTVNFfijOvGcvbV4zKSkpICsNCiAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSksIHRleHQgPSBlbGVtZW50X3RleHQoc2l6ZT0yMCkpDQoNCmdncGxvdChhY2N1cmFjeV9sb25nLGFlcyh4PWF2Zy50aW1lLHk9TlJNU0UsZmlsbD1hdmcudGltZSkpICsNCiAgZ2VvbV9ib3hwbG90KHNob3cubGVnZW5kID0gRikgKw0KICBnZW9tX2RvdHBsb3QoYmluYXhpcz0neScsIHN0YWNrZGlyPSdjZW50ZXInLCBkb3RzaXplPTEsIHNob3cubGVnZW5kPUYpICsNCiAgeGxhYigiQXZlcmFnaW5nIFRpbWUiKSArDQogIHlsYWIoIk5STVNFICglKSIpICsgDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKQ0KDQpgYGANCg0KIyMjIFNlbnNvci1TZW5zb3IgUHJlY2lzaW9uDQoNCldlIHdpbGwgY2FsY3VsYXRlIHNlbnNvci1zZW5zb3IgcHJlY2lzaW9uIG1ldHJpY3MgZm9yIGJvdGggaG91cmx5IGFuZCBkYWlseSBhdmVyYWdpbmcgcGVyaW9kcy4gV2Ugd2lsbCBhbHNvIHByb3ZpZGUgdGFidWxhciBzdGF0aXN0aWNzLCBhcyByZWNvbW1lbmRlZC4NClNEIGFuZCBDViBhcmUgY2FsY3VsYXRlZCBtYW51YWxseS4gQXQgdGhlIGVuZCBvZiB0aGlzIGNvZGUgY2h1bmsgdGhlIGRhdGEgYXJlIHJlcG9ydGVkIGluIHRhYnVsYXIgZm9ybWF0LiBTaW5jZSBvbmx5IG9uZSB2YWx1ZSBpcyByZXBvcnRlZCBmb3IgU0QgYW5kIENWICglKSwgdGhlcmUgYXJlIG5vIHBsb3RzLCBvbmx5IHRhYnVsYXIgZGF0YS4gDQoNCmBgYHtyIHByZWNpc2lvbn0NCiMjIDEgaG91cg0KDQpkZl8xaF9wcmVjaXNpb24gPC0gc3ByZWFkKGRmXzFoLExvY2F0aW9uLGRhdGFfMWgpIA0KDQojIENhbGN1bGF0ZSB0aGUgYXZlcmFnZSBhY3Jvc3MgYWxsIHRocmVlIFB1cnBsZUFpciBzZW5zb3JzIHVzaW5nIHJvd01lYW5zLiBTYXZlIGFzIG5ldyBjb2x1bW4gImF2Z18xaCINCmRmXzFoX3ByZWNpc2lvbiA8LSBkZl8xaF9wcmVjaXNpb24gJT4lDQogIG11dGF0ZSguLCBhdmdfMWggPSByb3dNZWFucyhzZWxlY3QoLiwgc3RhcnRzX3dpdGgoIkJ1cm5hYnkiKSksIG5hLnJtID0gVFJVRSkpIA0KDQojIENhbGN1bGF0ZSB0aGUgc3F1YXJlIHJlc2lkdWFsIG9mIGVhY2ggc2Vuc29yIHZzLiB0aGUgbWVhbiBhY3Jvc3Mgc2Vuc29ycw0KZGZfMWhfcHJlY2lzaW9uIDwtIGRmXzFoX3ByZWNpc2lvbiAlPiUNCiAgbXV0YXRlKC4sc3EucmVzaWQuQnVybmFieTEgPSAoQnVybmFieTEtYXZnXzFoKV4yLHNxLnJlc2lkLkJ1cm5hYnkyID0gKEJ1cm5hYnkyLWF2Z18xaCleMixzcS5yZXNpZC5CdXJuYWJ5MyA9IChCdXJuYWJ5My1hdmdfMWgpXjIpIA0KIA0KIyBDYWxjdWxhdGUgdGhlIHN1bSBvZiB0aGUgc3F1YXJlIHJlc2lkdWFsIGZvciBlYWNoIHNlbnNvcg0Kc3VtLnNxLnJlc2lkXzFoICA8LSBkZl8xaF9wcmVjaXNpb24gJT4lDQogIHN1bW1hcml6ZShzdW0uc3EucmVzaWQuQnVybmFieTEgPSBzdW0oc3EucmVzaWQuQnVybmFieTEpLA0KICAgICAgICAgICAgc3VtLnNxLnJlc2lkLkJ1cm5hYnkyID0gc3VtKHNxLnJlc2lkLkJ1cm5hYnkyKSwNCiAgICAgICAgICAgIHN1bS5zcS5yZXNpZC5CdXJuYWJ5MyA9IHN1bShzcS5yZXNpZC5CdXJuYWJ5MykpDQoNCiMgQ29tcHV0ZSBTRCBhbmQgQ1YgKCUpDQpTRF8xaCA9IHNxcnQoc3VtKHN1bS5zcS5yZXNpZF8xaCkvKCgzKm5yb3coZGZfMWhfcHJlY2lzaW9uKSktMSkpDQphdmdfMWhfYWxsIDwtIG1lYW4oZGZfMWhfcHJlY2lzaW9uJGF2Z18xaCkNCkNWXzFoIDwtIFNEXzFoIC8gYXZnXzFoX2FsbCAqIDEwMA0KDQojIyAyNCBob3VyDQpkZl8yNGhfcHJlY2lzaW9uIDwtIHNwcmVhZChkZl8yNGgsTG9jYXRpb24sZGF0YV8yNGgpIA0KDQojIENhbGN1bGF0ZSB0aGUgYXZlcmFnZSBhY3Jvc3MgYWxsIHRocmVlIFB1cnBsZUFpciBzZW5zb3JzIHVzaW5nIHJvd01lYW5zLiBTYXZlIGFzIG5ldyBjb2x1bW4gImF2Z18yNGgiDQpkZl8yNGhfcHJlY2lzaW9uIDwtIGRmXzI0aF9wcmVjaXNpb24gJT4lDQogIG11dGF0ZSguLCBhdmdfMjRoID0gcm93TWVhbnMoc2VsZWN0KC4sIHN0YXJ0c193aXRoKCJCdXJuYWJ5IikpLCBuYS5ybSA9IFRSVUUpKSANCg0KIyBDYWxjdWxhdGUgdGhlIHNxdWFyZSByZXNpZHVhbCBvZiBlYWNoIHNlbnNvciB2cy4gdGhlIG1lYW4gYWNyb3NzIHNlbnNvcnMNCmRmXzI0aF9wcmVjaXNpb24gPC0gZGZfMjRoX3ByZWNpc2lvbiAlPiUNCiAgbXV0YXRlKC4sc3EucmVzaWQuQnVybmFieTEgPSAoQnVybmFieTEtYXZnXzI0aCleMixzcS5yZXNpZC5CdXJuYWJ5MiA9IChCdXJuYWJ5Mi1hdmdfMjRoKV4yLHNxLnJlc2lkLkJ1cm5hYnkzID0gKEJ1cm5hYnkzLWF2Z18yNGgpXjIpIA0KDQojIENhbGN1bGF0ZSB0aGUgc3VtIG9mIHRoZSBzcXVhcmUgcmVzaWR1YWwgZm9yIGVhY2ggc2Vuc29yIA0Kc3VtLnNxLnJlc2lkXzI0aCAgPC0gZGZfMjRoX3ByZWNpc2lvbiAlPiUNCiAgc3VtbWFyaXplKHN1bS5zcS5yZXNpZC5CdXJuYWJ5MSA9IHN1bShzcS5yZXNpZC5CdXJuYWJ5MSksDQogICAgICAgICAgICBzdW0uc3EucmVzaWQuQnVybmFieTIgPSBzdW0oc3EucmVzaWQuQnVybmFieTIpLA0KICAgICAgICAgICAgc3VtLnNxLnJlc2lkLkJ1cm5hYnkzID0gc3VtKHNxLnJlc2lkLkJ1cm5hYnkzKSkNCg0KIyBDb21wdXRlIFNEIGFuZCBDViAoJSkNClNEXzI0aCA9IHNxcnQoc3VtKHN1bS5zcS5yZXNpZF8yNGgpLygoMypucm93KGRmXzI0aF9wcmVjaXNpb24pKS0xKSkNCmF2Z18yNGhfYWxsIDwtIG1lYW4oZGZfMjRoX3ByZWNpc2lvbiRhdmdfMjRoKQ0KQ1ZfMjRoIDwtIFNEXzI0aCAvIGF2Z18yNGhfYWxsICogMTAwDQoNCiMjIFByaW50IG1ldHJpY3MgYXMgdGFibGUNCg0KcHJlY2lzaW9uIDwtIGFzLnRpYmJsZShjYmluZChDVl8xaCxTRF8xaCxDVl8yNGgsU0RfMjRoKSkNCg0KcHJlY2lzaW9uICU+JQ0KICBrYmwoY2FwdGlvbj0iKipTZW5zb3IgUHJlY2lzaW9uIE1ldHJpY3MqKiIsIGRpZ2l0cz0yLCBjb2wubmFtZXMgPSBjKCJTRCAozrxnL21eM14pIiwiQ1YgKCUpIiwiU0QgKM68Zy9tXjNeKSIsIkNWICglKSIpKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkgJT4lDQogIGFkZF9oZWFkZXJfYWJvdmUoYygiMSBoIiA9IDIsICIyNCBoIiA9IDIpKQ0KYGBgDQojIyMgU2Vuc29yLUZFTSBBY2N1cmFjeTogU2VwYXJhdGluZyBIaWdoIENvbmNlbnRyYXRpb24gRXZlbnRzDQoNClNvbWUgb2Ygb3VyIHBlcmZvcm1hbmNlIG1ldHJpY3MgbWF5IGJlIHNrZXdlZCBieSBwZXJpb2RzIHdpdGggdmVyeSBoaWdoIGNvbmNlbnRyYXRpb25zIChlLmcuLCB3aWxkZmlyZXMpLiBXZSB3aWxsIGFsc28gY2FsY3VsYXRlIGFjY3VyYWN5IHBlcmZvcm1hbmNlIG1ldHJpY3Mgc2VwYXJhdGVseSBkdXJpbmcgaGlnaCBjb25jZW50cmF0aW9uIGVwaXNvZGVzIGFuZCBsb3dlciBjb25jZW50cmF0aW9uIHBlcmlvZHMuIEhlcmUgd2Ugd2lsbCBsb29rIGF0IHBlcmZvcm1hbmNlIHdoZW4gc3BsaXR0aW5nIGJhc2VkIG9uIHdoZXRoZXIgY29uY2VudHJhdGlvbnMgZXhjZWVkIDI1IMK1Zy9tXjNeLiBUaGUgcmVzdWx0cyB3aWxsIGJlIHJlcG9ydGVkIGluIHRhYnVsYXIgZm9ybWF0Lg0KDQpUaGlzIGlzIGNvZGVkIHZlcnkgc2ltaWxhcmx5IHRvIHRoZSBwcmV2aW91cyAiUGVyZm9ybWFuY2UgTWV0cmljcyIgc3Vic2VjdGlvbiAiU2Vuc29yLUZFTSBBY2N1cmFjeSIsIHdpdGggdGhlIGNhdmVhdCB0aGF0IGRwbHlyICJmaWx0ZXJzIiB3ZXJlIGFwcGxpZWQgdG8gc3BsaXQgdGhlIGRhdGEgYnkgdGhlIHJlcG9ydGVkIEZFTSBjb25jZW50cmF0aW9uLg0KDQpgYGB7ciBhY2N1cmFjeV9jYWxjdWxhdGlvbnNfc3BsaXR9DQoNCiMjIDEgaG91ciBkYXRhDQoNCiMgU2xvcGUsIGludGVyY2VwdCBhbmQgUjIgc3BsaXQgYnkgaGlnaCBhbmQgbG93IHVzaW5nICJmaWx0ZXIiLiBRdWlja2x5IGNhbGN1bGF0ZWQgdXNpbmcgUiBQYWNrYWdlICdicm9vbXMnIGFuZCB0aGVpciB0aWR5IGFuZCBnbGFuY2UgZnVuY3Rpb25zDQoNCm1vZGVsX3BhcmFtc18xaF9oaWdoIDwtIGRmXzFoX2FjY3VyYWN5ICU+JSBmaWx0ZXIoRkVNID49IDI1KSAlPiUgbmVzdChkYXRhID0gLUxvY2F0aW9uKSAlPiUgDQogIG11dGF0ZShtb2RlbCA9IG1hcChkYXRhLCB+bG0oZGF0YV8xaCB+IEZFTSwgZGF0YSA9IC4pKSwgdGlkaWVkID0gbWFwKG1vZGVsLCB0aWR5KSkgJT4lIHVubmVzdCh0aWRpZWQpIA0KDQptb2RlbF9wYXJhbXNfMWhfbG93IDwtIGRmXzFoX2FjY3VyYWN5ICU+JSBmaWx0ZXIoRkVNIDwgMjUpICU+JSBuZXN0KGRhdGEgPSAtTG9jYXRpb24pICU+JSANCiAgbXV0YXRlKG1vZGVsID0gbWFwKGRhdGEsIH5sbShkYXRhXzFoIH4gRkVNLCBkYXRhID0gLikpLCB0aWRpZWQgPSBtYXAobW9kZWwsIHRpZHkpKSAlPiUgdW5uZXN0KHRpZGllZCkgDQoNCm1vZGVsX3N0YXRzXzFoX2hpZ2ggPC0gZGZfMWhfYWNjdXJhY3kgJT4lIGZpbHRlcihGRU0gPj0gMjUpICU+JSBuZXN0KGRhdGEgPSAtTG9jYXRpb24pICU+JSANCiAgbXV0YXRlKG1vZGVsID0gbWFwKGRhdGEsIH5sbShkYXRhXzFoIH4gRkVNLCBkYXRhID0gLikpLCB0aWRpZWQgPSBtYXAobW9kZWwsIGdsYW5jZSkpICU+JSB1bm5lc3QodGlkaWVkKQ0KDQptb2RlbF9zdGF0c18xaF9sb3cgPC0gZGZfMWhfYWNjdXJhY3kgJT4lIGZpbHRlcihGRU0gPCAyNSkgJT4lIG5lc3QoZGF0YSA9IC1Mb2NhdGlvbikgJT4lIA0KICBtdXRhdGUobW9kZWwgPSBtYXAoZGF0YSwgfmxtKGRhdGFfMWggfiBGRU0sIGRhdGEgPSAuKSksIHRpZGllZCA9IG1hcChtb2RlbCwgZ2xhbmNlKSkgJT4lIHVubmVzdCh0aWRpZWQpDQoNCnNsb3Blc18xaF9oaWdoIDwtIG1vZGVsX3BhcmFtc18xaF9oaWdoICU+JSBmaWx0ZXIodGVybT09IkZFTSIpICU+JSBzZWxlY3QoTG9jYXRpb24sZXN0aW1hdGUpICU+JSByZW5hbWUoLixzbG9wZV8xaD1lc3RpbWF0ZSkNCmludGVyY2VwdHNfMWhfaGlnaCA8LSBtb2RlbF9wYXJhbXNfMWhfaGlnaCAlPiUgZmlsdGVyKHRlcm09PSIoSW50ZXJjZXB0KSIpICU+JSBzZWxlY3QoTG9jYXRpb24sZXN0aW1hdGUpICU+JSByZW5hbWUoLixpbnRlcmNlcHRfMWg9ZXN0aW1hdGUpDQpyMl8xaF9oaWdoIDwtIG1vZGVsX3N0YXRzXzFoX2hpZ2ggJT4lIHNlbGVjdChMb2NhdGlvbixyLnNxdWFyZWQpICU+JSByZW5hbWUoLixyMl8xaD1yLnNxdWFyZWQpDQoNCnNsb3Blc18xaF9sb3cgPC0gbW9kZWxfcGFyYW1zXzFoX2xvdyAlPiUgZmlsdGVyKHRlcm09PSJGRU0iKSAlPiUgc2VsZWN0KExvY2F0aW9uLGVzdGltYXRlKSAlPiUgcmVuYW1lKC4sc2xvcGVfMWg9ZXN0aW1hdGUpDQppbnRlcmNlcHRzXzFoX2xvdyA8LSBtb2RlbF9wYXJhbXNfMWhfbG93ICU+JSBmaWx0ZXIodGVybT09IihJbnRlcmNlcHQpIikgJT4lIHNlbGVjdChMb2NhdGlvbixlc3RpbWF0ZSkgJT4lIHJlbmFtZSguLGludGVyY2VwdF8xaD1lc3RpbWF0ZSkNCnIyXzFoX2xvdyA8LSBtb2RlbF9zdGF0c18xaF9sb3cgJT4lIHNlbGVjdChMb2NhdGlvbixyLnNxdWFyZWQpICU+JSByZW5hbWUoLixyMl8xaD1yLnNxdWFyZWQpDQoNCiMgUk1TRSBhbmQgTlJNU0U6IE1hbnVhbCBjYWxjdWxhdGlvbnMNCmF2Z18xaF9oaWdoIDwtZGZfMWhfYWNjdXJhY3kgJT4lDQogIGZpbHRlcihGRU0gPj0gMjUpICU+JQ0KICBzdW1tYXJpemUobWVhbihGRU0sbmEucm09VCkpDQoNCg0KUk1TRV8xaF9oaWdoIDwtIGRmXzFoX2FjY3VyYWN5ICU+JQ0KICBmaWx0ZXIoRkVNID49IDI1KSAlPiUNCiAgZ3JvdXBfYnkoTG9jYXRpb24pICU+JQ0KICBzdW1tYXJpemUoUk1TRV8xaD1zcXJ0KG1lYW4oc3EucmVzaWR1YWwsbmEucm09VCkpLA0KICAgICAgICAgICAgTlJNU0VfMWg9c3FydChtZWFuKHNxLnJlc2lkdWFsLG5hLnJtPVQpKS9tZWFuKEZFTSxuYS5ybT1UKSoxMDApDQoNClJNU0VfMWhfbG93IDwtIGRmXzFoX2FjY3VyYWN5ICU+JQ0KICBmaWx0ZXIoRkVNIDwgMjUpICU+JQ0KICBncm91cF9ieShMb2NhdGlvbikgJT4lDQogIHN1bW1hcml6ZShSTVNFXzFoPXNxcnQobWVhbihzcS5yZXNpZHVhbCxuYS5ybT1UKSksDQogICAgICAgICAgICBOUk1TRV8xaD1zcXJ0KG1lYW4oc3EucmVzaWR1YWwsbmEucm09VCkpL21lYW4oRkVNLG5hLnJtPVQpKjEwMCkNCg0KYWNjdXJhY3lfMWhfbG93IDwtICBsZWZ0X2pvaW4ocjJfMWhfbG93LCBzbG9wZXNfMWhfbG93LCBieT0nTG9jYXRpb24nKSAlPiUNCiAgICAgICAgICAgICAgICBsZWZ0X2pvaW4oLiwgaW50ZXJjZXB0c18xaF9sb3csIGJ5PSdMb2NhdGlvbicpICU+JQ0KICAgICAgICAgICAgICAgIGxlZnRfam9pbiguLFJNU0VfMWhfbG93LGJ5PSdMb2NhdGlvbicpDQoNCmFjY3VyYWN5XzFoX2hpZ2ggPC0gIGxlZnRfam9pbihyMl8xaF9oaWdoLCBzbG9wZXNfMWhfaGlnaCwgYnk9J0xvY2F0aW9uJykgJT4lDQogICAgICAgICAgICAgICAgbGVmdF9qb2luKC4sIGludGVyY2VwdHNfMWhfaGlnaCwgYnk9J0xvY2F0aW9uJykgJT4lDQogICAgICAgICAgICAgICAgbGVmdF9qb2luKC4sUk1TRV8xaF9oaWdoLGJ5PSdMb2NhdGlvbicpDQoNCiMjIDI0IGhvdXIgZGF0YQ0KDQojIFNsb3BlLCBpbnRlcmNlcHQgYW5kIFIyIHNwbGl0IGJ5IGhpZ2ggYW5kIGxvdyB1c2luZyAiZmlsdGVyIi4gUXVpY2tseSBjYWxjdWxhdGVkIHVzaW5nIFIgUGFja2FnZSAnYnJvb21zJyBhbmQgdGhlaXIgdGlkeSBhbmQgZ2xhbmNlIGZ1bmN0aW9ucw0KDQptb2RlbF9wYXJhbXNfMjRoX2hpZ2ggPC0gZGZfMjRoX2FjY3VyYWN5ICU+JSBmaWx0ZXIoRkVNID49IDI1KSAlPiUgbmVzdChkYXRhID0gLUxvY2F0aW9uKSAlPiUgDQogIG11dGF0ZShtb2RlbCA9IG1hcChkYXRhLCB+bG0oZGF0YV8yNGggfiBGRU0sIGRhdGEgPSAuKSksIHRpZGllZCA9IG1hcChtb2RlbCwgdGlkeSkpICU+JSB1bm5lc3QodGlkaWVkKSANCg0KbW9kZWxfcGFyYW1zXzI0aF9sb3cgPC0gZGZfMjRoX2FjY3VyYWN5ICU+JSBmaWx0ZXIoRkVNIDwgMjUpICU+JSBuZXN0KGRhdGEgPSAtTG9jYXRpb24pICU+JSANCiAgbXV0YXRlKG1vZGVsID0gbWFwKGRhdGEsIH5sbShkYXRhXzI0aCB+IEZFTSwgZGF0YSA9IC4pKSwgdGlkaWVkID0gbWFwKG1vZGVsLCB0aWR5KSkgJT4lIHVubmVzdCh0aWRpZWQpIA0KDQptb2RlbF9zdGF0c18yNGhfaGlnaCA8LSBkZl8yNGhfYWNjdXJhY3kgJT4lIGZpbHRlcihGRU0gPj0gMjUpICU+JSBuZXN0KGRhdGEgPSAtTG9jYXRpb24pICU+JSANCiAgbXV0YXRlKG1vZGVsID0gbWFwKGRhdGEsIH5sbShkYXRhXzI0aCB+IEZFTSwgZGF0YSA9IC4pKSwgdGlkaWVkID0gbWFwKG1vZGVsLCBnbGFuY2UpKSAlPiUgdW5uZXN0KHRpZGllZCkNCg0KbW9kZWxfc3RhdHNfMjRoX2xvdyA8LSBkZl8yNGhfYWNjdXJhY3kgJT4lIGZpbHRlcihGRU0gPCAyNSkgJT4lIG5lc3QoZGF0YSA9IC1Mb2NhdGlvbikgJT4lIA0KICBtdXRhdGUobW9kZWwgPSBtYXAoZGF0YSwgfmxtKGRhdGFfMjRoIH4gRkVNLCBkYXRhID0gLikpLCB0aWRpZWQgPSBtYXAobW9kZWwsIGdsYW5jZSkpICU+JSB1bm5lc3QodGlkaWVkKQ0KDQpzbG9wZXNfMjRoX2hpZ2ggPC0gbW9kZWxfcGFyYW1zXzI0aF9oaWdoICU+JSBmaWx0ZXIodGVybT09IkZFTSIpICU+JSBzZWxlY3QoTG9jYXRpb24sZXN0aW1hdGUpICU+JSByZW5hbWUoLixzbG9wZV8yNGg9ZXN0aW1hdGUpDQppbnRlcmNlcHRzXzI0aF9oaWdoIDwtIG1vZGVsX3BhcmFtc18yNGhfaGlnaCAlPiUgZmlsdGVyKHRlcm09PSIoSW50ZXJjZXB0KSIpICU+JSBzZWxlY3QoTG9jYXRpb24sZXN0aW1hdGUpICU+JSByZW5hbWUoLixpbnRlcmNlcHRfMjRoPWVzdGltYXRlKQ0KcjJfMjRoX2hpZ2ggPC0gbW9kZWxfc3RhdHNfMjRoX2hpZ2ggJT4lIHNlbGVjdChMb2NhdGlvbixyLnNxdWFyZWQpICU+JSByZW5hbWUoLixyMl8yNGg9ci5zcXVhcmVkKQ0KDQpzbG9wZXNfMjRoX2xvdyA8LSBtb2RlbF9wYXJhbXNfMjRoX2xvdyAlPiUgZmlsdGVyKHRlcm09PSJGRU0iKSAlPiUgc2VsZWN0KExvY2F0aW9uLGVzdGltYXRlKSAlPiUgcmVuYW1lKC4sc2xvcGVfMjRoPWVzdGltYXRlKQ0KaW50ZXJjZXB0c18yNGhfbG93IDwtIG1vZGVsX3BhcmFtc18yNGhfbG93ICU+JSBmaWx0ZXIodGVybT09IihJbnRlcmNlcHQpIikgJT4lIHNlbGVjdChMb2NhdGlvbixlc3RpbWF0ZSkgJT4lIHJlbmFtZSguLGludGVyY2VwdF8yNGg9ZXN0aW1hdGUpDQpyMl8yNGhfbG93IDwtIG1vZGVsX3N0YXRzXzI0aF9sb3cgJT4lIHNlbGVjdChMb2NhdGlvbixyLnNxdWFyZWQpICU+JSByZW5hbWUoLixyMl8yNGg9ci5zcXVhcmVkKQ0KDQojIFJNU0UgYW5kIE5STVNFOiBNYW51YWwgY2FsY3VsYXRpb25zDQoNClJNU0VfMjRoX2hpZ2ggPC0gZGZfMjRoX2FjY3VyYWN5ICU+JQ0KICBmaWx0ZXIoRkVNID49IDI1KSAlPiUNCiAgZ3JvdXBfYnkoTG9jYXRpb24pICU+JQ0KICBzdW1tYXJpemUoUk1TRV8yNGg9c3FydChtZWFuKHNxLnJlc2lkdWFsLG5hLnJtPVQpKSwNCiAgICAgICAgICAgIE5STVNFXzI0aD1zcXJ0KG1lYW4oc3EucmVzaWR1YWwsbmEucm09VCkpL21lYW4oRkVNLG5hLnJtPVQpKjEwMCkNCg0KUk1TRV8yNGhfbG93IDwtIGRmXzI0aF9hY2N1cmFjeSAlPiUNCiAgZmlsdGVyKEZFTSA8IDI1KSAlPiUNCiAgZ3JvdXBfYnkoTG9jYXRpb24pICU+JQ0KICBzdW1tYXJpemUoUk1TRV8yNGg9c3FydChtZWFuKHNxLnJlc2lkdWFsLG5hLnJtPVQpKSwNCiAgICAgICAgICAgIE5STVNFXzI0aD1zcXJ0KG1lYW4oc3EucmVzaWR1YWwsbmEucm09VCkpL21lYW4oRkVNLG5hLnJtPVQpKjEwMCkNCg0KYWNjdXJhY3lfMjRoX2xvdyA8LSAgbGVmdF9qb2luKHIyXzI0aF9sb3csIHNsb3Blc18yNGhfbG93LCBieT0nTG9jYXRpb24nKSAlPiUNCiAgICAgICAgICAgICAgICBsZWZ0X2pvaW4oLiwgaW50ZXJjZXB0c18yNGhfbG93LCBieT0nTG9jYXRpb24nKSAlPiUNCiAgICAgICAgICAgICAgICBsZWZ0X2pvaW4oLixSTVNFXzI0aF9sb3csYnk9J0xvY2F0aW9uJykNCg0KYWNjdXJhY3lfMjRoX2hpZ2ggPC0gIGxlZnRfam9pbihyMl8yNGhfaGlnaCwgc2xvcGVzXzI0aF9oaWdoLCBieT0nTG9jYXRpb24nKSAlPiUNCiAgICAgICAgICAgICAgICBsZWZ0X2pvaW4oLiwgaW50ZXJjZXB0c18yNGhfaGlnaCwgYnk9J0xvY2F0aW9uJykgJT4lDQogICAgICAgICAgICAgICAgbGVmdF9qb2luKC4sUk1TRV8yNGhfaGlnaCxieT0nTG9jYXRpb24nKQ0KDQojIyMjIw0KDQphY2N1cmFjeV9sb3cgPC0gbGVmdF9qb2luKGFjY3VyYWN5XzFoX2xvdyxhY2N1cmFjeV8yNGhfbG93LGJ5PSdMb2NhdGlvbicpDQphY2N1cmFjeV9oaWdoIDwtIGxlZnRfam9pbihhY2N1cmFjeV8xaF9oaWdoLGFjY3VyYWN5XzI0aF9oaWdoLGJ5PSdMb2NhdGlvbicpDQoNCiMgV2Ugd2lsbCB1c2UgdGhpcyBncm91cGVkIGRhdGEgbGF0ZXIgd2hlbiBwbG90dGluZw0KYWNjdXJhY3lfbG9uZ19oaWdoIDwtIG1lbHQoc2V0RFQoYWNjdXJhY3lfaGlnaCksaWQudmFycz0nTG9jYXRpb24nLG1lYXN1cmUudmFycz1wYXR0ZXJucygiXnIyXyIsIl5zbG9wZV8iLCJeaW50ZXJjZXB0XyIsIl5STVNFXyIsIl5OUk1TRV8iKSx2YWx1ZS5uYW1lID0gYygicjIiLCAic2xvcGUiLCJpbnRlcmNlcHQiLCJSTVNFIiwiTlJNU0UiKSx2YXJpYWJsZS5uYW1lID0gImF2Zy50aW1lIikNCmFjY3VyYWN5X2xvbmdfaGlnaCRhdmcudGltZSA8LSByZWNvZGVfZmFjdG9yKGFjY3VyYWN5X2xvbmdfaGlnaCRhdmcudGltZSxgMWA9IjEgaCIsYDJgPSIyNCBoIikNCg0KYWNjdXJhY3lfbG9uZ19sb3cgPC0gbWVsdChzZXREVChhY2N1cmFjeV9oaWdoKSxpZC52YXJzPSdMb2NhdGlvbicsbWVhc3VyZS52YXJzPXBhdHRlcm5zKCJecjJfIiwiXnNsb3BlXyIsIl5pbnRlcmNlcHRfIiwiXlJNU0VfIiwiXk5STVNFXyIpLHZhbHVlLm5hbWUgPSBjKCJyMiIsICJzbG9wZSIsImludGVyY2VwdCIsIlJNU0UiLCJOUk1TRSIpLHZhcmlhYmxlLm5hbWUgPSAiYXZnLnRpbWUiKQ0KYWNjdXJhY3lfbG9uZ19sb3ckYXZnLnRpbWUgPC0gcmVjb2RlX2ZhY3RvcihhY2N1cmFjeV9sb25nX2xvdyRhdmcudGltZSxgMWA9IjEgaCIsYDJgPSIyNCBoIikNCg0KDQojIyBQcmludCBtZXRyaWNzIGFzIHRhYmxlDQoNCmFjY3VyYWN5X21lYW5faGlnaCA8LSBhY2N1cmFjeV9oaWdoICU+JSBzdW1tYXJpemVfaWYoaXMubnVtZXJpYyxtZWFuKQ0KYWNjdXJhY3lfaGlnaCA8LSBiaW5kX3Jvd3MoYWNjdXJhY3lfaGlnaCxhY2N1cmFjeV9tZWFuX2hpZ2gpDQphY2N1cmFjeV9oaWdoWzQsMV0gPSAiTWVhbiINCg0KYWNjdXJhY3lfaGlnaCAlPiUNCiAga2JsKGNhcHRpb249IioqU2Vuc29yLUZFTSBBY2N1cmFjeSBNZXRyaWNzLCA+PTI1IM68Zy9tXjNeKioiLCBkaWdpdHM9MiwgY29sLm5hbWVzID0gYygiU2Vuc29yIElEIiwiUl4yXiIsIlNsb3BlIiwiSW50ZXJjZXB0IiwiUk1TRSIsIk5STVNFIiwiUl4yXiIsIlNsb3BlIiwiSW50ZXJjZXB0IiwiUk1TRSIsIk5STVNFIikpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKSAlPiUNCiAgYWRkX2hlYWRlcl9hYm92ZShjKCIgIiA9IDEsICIxIGgiID0gNSwgIjI0IGgiID0gNSkpDQoNCmFjY3VyYWN5X21lYW5fbG93IDwtIGFjY3VyYWN5X2xvdyAlPiUgc3VtbWFyaXplX2lmKGlzLm51bWVyaWMsbWVhbikNCmFjY3VyYWN5X2xvdyA8LSBiaW5kX3Jvd3MoYWNjdXJhY3lfbG93LGFjY3VyYWN5X21lYW5fbG93KQ0KYWNjdXJhY3lfbG93WzQsMV0gPSAiTWVhbiINCg0KYWNjdXJhY3lfbG93ICU+JQ0KICBrYmwoY2FwdGlvbj0iKipTZW5zb3ItRkVNIEFjY3VyYWN5IE1ldHJpY3MsIDwyNSDOvGcvbV4zXioqIiwgZGlnaXRzPTIsIGNvbC5uYW1lcyA9IGMoIlNlbnNvciBJRCIsIlJeMl4iLCJTbG9wZSIsIkludGVyY2VwdCIsIlJNU0UiLCJOUk1TRSIsIlJeMl4iLCJTbG9wZSIsIkludGVyY2VwdCIsIlJNU0UiLCJOUk1TRSIpKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkgJT4lDQogIGFkZF9oZWFkZXJfYWJvdmUoYygiICIgPSAxLCAiMSBoIiA9IDUsICIyNCBoIiA9IDUpKQ0KDQpgYGANCg0KSW4gdGhpcyBzY2VuYXJpbywgdGhlIHNwbGl0IGFuYWx5c2lzIHByb3ZpZGVzIHVzZWZ1bCBpbnNpZ2h0OiB0aGUgUHVycGxlQWlyIG1vbml0b3JzIGFyZSB0ZW5kaW5nIHRvIG92ZXJlc3RpbWF0ZSBhdCBsb3cgY29uY2VudHJhdGlvbnMgKHNsb3BlcyA+IDEpIGFuZCB1bmRlcmVzdGltYXRlIGR1cmluZyB0aGUgd2lsZGZpcmUgcGVyaW9kIChzbG9wZXMgPCAxKS4gVGhpcyBzdWdnZXN0cyB3ZSBtaWdodCBhbHNvIG5lZWQgcGllY2Utd2lzZSBjYWxpYnJhdGlvbnMgZm9yIGxvdyBhbmQgaGlnaCBjb25jZW50cmF0aW9ucywgc3VjaCBhcyB3aGF0IGlzIGRpc2N1c3NlZCBpbiB0aGUgbGl0ZXJhdHVyZSBbQE1hbGluZ3MyMDIwXS4gV2Ugd2lsbCBkZW1vbnN0cmF0ZSB0aGlzIGluIFBhcnQgMy4gV2UgYWxzbyBzZWUgdGhhdCB0aGUgc2Vuc29ycyBzZWVtIHRvIGJlIG1vcmUgYWNjdXJhdGUgYXQgaGlnaCBjb25jZW50cmF0aW9uOyBOUk1TRSBpcyBnZW5lcmFsbHkgYmVsb3cgMzAlIGZvciBoaWdoIGNvbmNlbnRyYXRpb24gZGF0YS4NCg0KIyMgTWV0ZW9yb2xvZ2ljYWwgQ29uZGl0aW9ucyBEdXJpbmcgRGVwbG95bWVudA0KDQpXZSB3aWxsIG5lZWQgdG8gZG93bmxvYWQgc29tZSBtZXRlb3JvbG9naWNhbCBzdGF0aW9uIGRhdGEgdG8gcmVwb3J0IG9uIHRoZSBtZXRlb3JvbG9naWNhbCBjb25kaXRpb25zIGR1cmluZyB0aGUgZGVwbG95bWVudC4gSXQgaXMgaW1wb3J0YW50IHRvIG5vdGUgdGhhdCB0aGUgaW50ZXJuYWwgdGVtcGVyYXR1cmUgYW5kIHJlbGF0aXZlIGh1bWlkaXR5IHNlbnNvcnMgaW5zaWRlIGxvdy1jb3N0IHNlbnNvciB1bml0cyBhcmUgb2Z0ZW4gaW5mbHVlbmNlZCBieSB0aGVpciBpbnRlcm5hbCBlbnZpcm9ubWVudC4gSW4gZ2VuZXJhbCBpdCBpcyBzdHJvbmdseSBwcmVmZXJyZWQgdG8gcmVwb3J0IGV4dGVybmFsbHkgbW9uaXRvcmVkIHdlYXRoZXIgZGF0YS4gDQoNClRvIGdldCByZWd1bGF0b3J5IHdlYXRoZXIgc3RhdGlvbiBkYXRhIGluIENhbmFkYSwgd2Ugd2lsbCB1c2UgdGhlICJ3ZWF0aGVyY2FuIiBwYWNrYWdlIFtAUi13ZWF0aGVyY2FuXS4gSWYgeW91IHdhbnQgb3RoZXIgbWV0ZW9yb2xvZ2ljYWwgZGF0YSBmb3Igb3RoZXIgcGFydHMgb2YgdGhlIHdvcmxkLCB0aGlzIHNlY3Rpb24gb2YgY29kZSB3b3VsZCBuZWVkIHRvIGJlIGFkYXB0ZWQsIGJ1dCB0aGF0IGlzIG91dHNpZGUgdGhlIHNjb3BlIG9mIHRoaXMgdHV0b3JpYWwuIENvbnNpZGVyIGNoZWNraW5nIHlvdXIgbG9jYWwgcmVndWxhdG9yeSBtb25pdG9yaW5nIG5ldHdvcmsgb3IgdXNpbmcgUiBwYWNrYWdlcyBkZXNpZ25lZCB0byBpbXBvcnQgd2VhdGhlciBmcm9tIGVsc2V3aGVyZSAoZS5nLiwgInJub2FhIiBmb3IgTk9BQSBkYXRhKS4NCg0KSW4gdGhpcyBzZWN0aW9uLCB3ZSBkb3dubG9hZCB0aGUgMSBoIHdlYXRoZXIgZGF0YSwgY29udmVydCBpdCB0byAyNCBoIGF2ZXJhZ2VzIHVzaW5nIHRoZSAidGltZUF2ZXJhZ2UiIGZ1bmN0aW9uIGluIHRoZSBSIFBhY2thZ2UgIm9wZW5haXIiIFtAUi1vcGVuYWlyXSwgYW5kIHBsb3QgaGlzdG9ncmFtcy4gDQoNCmBgYHtyIG1ldF9jb25kaXRpb24sIGZpZy5zaG93PSJob2xkIiwgb3V0LndpZHRoPSI1MCUifQ0KDQpjbG9zZXN0X3N0YXRpb24gPC0gc3RhdGlvbnNfc2VhcmNoKGNvb3Jkcz1jKDQ5LjIxNjIsLTEyMi45ODUyKSwgaW50ZXJ2YWw9ImhvdXIiLCBlbmRzX2VhcmxpZXN0ID0gIjIwMjEiKQ0KDQp3ZWF0aGVyXzFoIDwtIHdlYXRoZXJfZGwoc3RhdGlvbl9pZHMgPSBjbG9zZXN0X3N0YXRpb24kc3RhdGlvbl9pZCxzdGFydD0iMjAyMC0wOS0wMSIsZW5kPSIyMDIwLTEyLTMxIixpbnRlcnZhbD0iaG91ciIpDQp3ZWF0aGVyXzFoIDwtIHdlYXRoZXJfMWggJT4lIHNlbGVjdCh0aW1lLHJlbF9odW0sdGVtcCx0ZW1wX2RldykgJT4lIHJlbmFtZSguLGRhdGU9dGltZSkNCg0Kd2VhdGhlcl8yNGggPC0gdGltZUF2ZXJhZ2Uod2VhdGhlcl8xaCxhdmcudGltZT0iZGF5IikNCg0KZ2dwbG90KHdlYXRoZXJfMjRoLCBhZXMoeCA9IHRlbXApKSArIA0KICBnZW9tX2hpc3RvZ3JhbShhZXMoeSA9IHN0YXQoY291bnQpIC8gc3VtKGNvdW50KSksIGJpbnMgPSA4LGNvbG91cj0iYmxhY2siLGZpbGw9ImRhcmtnb2xkZW5yb2QxIikgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50KSArDQogIHhsYWIoIlRlbXBlcmF0dXJlICjCsEMpIikgKw0KICB5bGFiKCJSZWxhdGl2ZSBQcm9iYWJpbGl0eSIpICsNCiAgdGhlbWUocGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChoanVzdCA9IDAuNSksIHRleHQgPSBlbGVtZW50X3RleHQoc2l6ZT0yMCkpDQoNCmdncGxvdCh3ZWF0aGVyXzI0aCwgYWVzKHggPSByZWxfaHVtKSkgKyANCiAgZ2VvbV9oaXN0b2dyYW0oYWVzKHkgPSBzdGF0KGNvdW50KSAvIHN1bShjb3VudCkpLCBiaW5zID0gMTIsY29sb3VyPSJibGFjayIsZmlsbD0iZGFya29saXZlZ3JlZW4zIikgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzID0gc2NhbGVzOjpwZXJjZW50KSArDQogIHhsYWIoIlJlbGF0aXZlIEh1bWlkaXR5ICglKSIpICsNCiAgeWxhYigiUmVsYXRpdmUgUHJvYmFiaWxpdHkiKSArDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKQ0KDQpgYGANCkZyb20gdGhlc2UgcGxvdHMgbm90ZSB0aGF0IGEgbG90IG9mIG91ciBkYXRhIGlzIGF0IGhpZ2ggcmVsYXRpdmUgaHVtaWRpdHkgLSBpdCByYWlucyBhIGxvdCBpbiBWYW5jb3V2ZXIhIEhpZ2ggcmVsYXRpdmUgaHVtaWRpdHkgaXMga25vd24gdG8gcmVzdWx0IGluIG92ZXJlc3RpbWF0ZWQgbG93LWNvc3QgUE1+Mi41fiBzZW5zb3IgcmVhZGluZ3MgW0BNYWxpbmdzMjAyMF0uDQoNCiMjIE1ldGVvcm9sb2dpY2FsIEluZmx1ZW5jZQ0KDQpJbiB0aGlzIHNlY3Rpb24sIHdlIHBsb3QgdGhlIG5vcm1hbGl6ZWQgUE1+Mi41fiBjb25jZW50cmF0aW9uIChkZWZpbmVkIGFzIFNlbnNvciBQTX4yLjV+IC8gRkVNIFBNfjIuNX4pIGZvciBlYWNoIDI0IGhvdXIgcGVyaW9kIGFnYWluc3QgdGhlIG1vbml0b3JlZCB0ZW1wZXJhdHVyZSBhbmQgcmVsYXRpdmUgaHVtaWRpdHkuIFdlIHdpbGwgYWxzbyBzaG93IHNjYXR0ZXIgcGxvdHMgb2YgRkVNIHZzIFNlbnNvciBQTX4yLjV+IGFzIGNvbG91cmVkIGJ5IHJlbGF0aXZlIGh1bWlkaXR5LiBUbyBkbyB0aGlzLCB3ZSBtdXN0IGZpcnN0IGpvaW4gb3VyIHdlYXRoZXIgZGF0YSB0byBvdXIgc2Vuc29yIGRhdGEgYW5kIHRoZW4gY2FsY3VsYXRlIG5vcm1hbGl6ZWQgY29uY2VudHJhdGlvbnMuIFdoZW4gd2UgYXJlIGZpbmlzaGVkLCB3ZSB3aWxsIHBsb3QgdGhlIHJlc3VsdHMuDQoNCmBgYHtyIG1ldF9pbmZsdWVuY2UsIGZpZy5zaG93PSJob2xkIiwgb3V0LndpZHRoPSI1MCUifQ0KDQojIyBQcmVwIHRoZSBkYXRhIC0gQWRkIHdlYXRoZXIgZGF0YSBhbmQgY2FsY3VsYXRlIG5vcm1hbGl6ZWQgY29uY2VudHJhdGlvbnMNCg0Kd2VhdGhlcl8yNGggPC0gcmVuYW1lKHdlYXRoZXJfMjRoLGRhdGV0aW1lXzI0aCA9IGRhdGUpDQpkZl8yNGhfYWxsIDwtIGZ1bGxfam9pbihkZl8yNGhfcHJlY2lzaW9uLHdlYXRoZXJfMjRoLGJ5PSJkYXRldGltZV8yNGgiKSAlPiUgc2VsZWN0KC1hdmdfMjRoLC1zcS5yZXNpZC5CdXJuYWJ5MSwtc3EucmVzaWQuQnVybmFieTIsLXNxLnJlc2lkLkJ1cm5hYnkzKQ0KDQojIEFkZCBuZXcgY29sdW1ucyBmb3Igbm9ybWFsaXplZCBQTTIuNSBkYXRhDQpkZl8yNGhfYWxsIDwtIGRmXzI0aF9hbGwgJT4lDQogIG11dGF0ZSguLG5vcm1fQnVybmFieTE9QnVybmFieTEvRkVNLG5vcm1fQnVybmFieTI9QnVybmFieTIvRkVNLG5vcm1fQnVybmFieTMgPSBCdXJuYWJ5My9GRU0pDQoNCiMgTGV0J3MgcGl2b3QgdGhpcyBpbnRvIGEgY29udmVuaWVudCBmb3JtYXQgZm9yIHBsb3R0aW5nDQpkZl8yNGhfYWxsX2xvbmcgPC0gZGZfMjRoX2FsbCAlPiUgDQogIHBpdm90X2xvbmdlcihjb2xzPXN0YXJ0c193aXRoKCJub3JtXyIpLG5hbWVzX3RvPSJMb2NhdGlvbiIsbmFtZXNfcHJlZml4PSJub3JtXyIsdmFsdWVzX3RvPSJub3JtX2NvbmMiKQ0KDQojIyBQbG90IHRoZSBkYXRhDQoNCmdncGxvdChkZl8yNGhfYWxsX2xvbmcsYWVzKHg9dGVtcCx5PW5vcm1fY29uYyxncm91cD1Mb2NhdGlvbixjb2xvcj1Mb2NhdGlvbikpICsNCiAgZ2VvbV9wb2ludChhbHBoYT0wLjcsc2l6ZT0yKSArDQogIHlsaW0oLTEsNSkgKw0KICB4bGFiKCJUZW1wZXJhdHVyZSAowrBDKSIpICsNCiAgeWxhYigiTm9ybWFsaXplZCBDb25jZW50cmF0aW9uIikgKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQ9MSxsaW5ldHlwZT0iZGFzaGVkIikgKw0KICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSwgdGV4dCA9IGVsZW1lbnRfdGV4dChzaXplPTIwKSkNCg0KZ2dwbG90KGRmXzI0aF9hbGxfbG9uZyxhZXMoeD1yZWxfaHVtLHk9bm9ybV9jb25jLGdyb3VwPUxvY2F0aW9uLGNvbG9yPUxvY2F0aW9uKSkgKw0KICBnZW9tX3BvaW50KGFscGhhPTAuNyxzaXplPTIpICsNCiAgeWxpbSgtMSw1KSArDQogIHhsYWIoIlJlbGF0aXZlIEh1bWlkaXR5ICglKSIpICsNCiAgeWxhYigiTm9ybWFsaXplZCBDb25jZW50cmF0aW9uIikgKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQ9MSxsaW5ldHlwZT0iZGFzaGVkIikgKw0KICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSwgdGV4dCA9IGVsZW1lbnRfdGV4dChzaXplPTIwKSkNCg0KDQojIyBJbmRpdmlkdWFsIFNlbnNvci1GRU0gc2NhdHRlciBwbG90cyBjb2xvdXJlZCBieSByZWxhdGl2ZSBodW1pZGl0eQ0KDQpnZ3Bsb3QoZGZfMjRoX2FsbCxhZXMoeD1GRU0seT1CdXJuYWJ5MSxjb2xvdXI9cmVsX2h1bSkpICsNCiAgZ2VvbV9wb2ludChzaXplPTIpICsNCiAgc2NhbGVfY29sb3VyX2dyYWRpZW50Mihsb3c9ImJsdWUiLCBtaWQ9IndoaXRlIiwgaGlnaD0icmVkIiwgbWlkcG9pbnQ9NTAsbGltaXRzPWMoMCwxMDApKSArDQogIGxhYnModGl0bGU9IkJ1cm5hYnkgMSIsIHg9ZXhwcmVzc2lvbihGRU0vRlJNflBNWzIuNV1+Q29uY2VudHJhdGlvbn4ozrxnL21eMykpLCB5PWV4cHJlc3Npb24oU2Vuc29yflBNWzIuNV1+Q29uY2VudHJhdGlvbn4ozrxnL21eMykpLCBjb2xvcj0iUkggKCUpIikgKw0KICBnZW9tX2FibGluZShzbG9wZT0xLGxpbmV0eXBlPSJkYXNoZWQiKSArDQogIHhsaW0oMCwzMCkgKw0KICB5bGltKDAsMzApICsNCiAgY29vcmRfZXF1YWwoKSArDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKQ0KDQpnZ3Bsb3QoZGZfMjRoX2FsbCxhZXMoeD1GRU0seT1CdXJuYWJ5Mixjb2xvdXI9cmVsX2h1bSkpICsNCiAgZ2VvbV9wb2ludChzaXplPTIpICsNCiAgc2NhbGVfY29sb3VyX2dyYWRpZW50Mihsb3c9ImJsdWUiLCBtaWQ9IndoaXRlIiwgaGlnaD0icmVkIiwgbWlkcG9pbnQ9NTAsbGltaXRzPWMoMCwxMDApKSArDQogIGxhYnModGl0bGU9IkJ1cm5hYnkgMiIsIHg9ZXhwcmVzc2lvbihGRU0vRlJNflBNWzIuNV1+Q29uY2VudHJhdGlvbn4ozrxnL21eMykpLCB5PWV4cHJlc3Npb24oU2Vuc29yflBNWzIuNV1+Q29uY2VudHJhdGlvbn4ozrxnL21eMykpLCBjb2xvcj0iUkggKCUpIikgKw0KICBnZW9tX2FibGluZShzbG9wZT0xLGxpbmV0eXBlPSJkYXNoZWQiKSArDQogIHhsaW0oMCwzMCkgKw0KICB5bGltKDAsMzApICsNCiAgY29vcmRfZXF1YWwoKSArDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKQ0KDQpnZ3Bsb3QoZGZfMjRoX2FsbCxhZXMoeD1GRU0seT1CdXJuYWJ5Myxjb2xvdXI9cmVsX2h1bSkpICsNCiAgZ2VvbV9wb2ludChzaXplPTIpICsNCiAgc2NhbGVfY29sb3VyX2dyYWRpZW50Mihsb3c9ImJsdWUiLCBtaWQ9IndoaXRlIiwgaGlnaD0icmVkIiwgbWlkcG9pbnQ9NTAsbGltaXRzPWMoMCwxMDApKSArDQogIGxhYnModGl0bGU9IkJ1cm5hYnkgMyIsIHg9ZXhwcmVzc2lvbihGRU0vRlJNflBNWzIuNV1+Q29uY2VudHJhdGlvbn4ozrxnL21eMykpLCB5PWV4cHJlc3Npb24oU2Vuc29yflBNWzIuNV1+Q29uY2VudHJhdGlvbn4ozrxnL21eMykpLCBjb2xvcj0iUkggKCUpIikgKw0KICBnZW9tX2FibGluZShzbG9wZT0xLGxpbmV0eXBlPSJkYXNoZWQiKSArDQogIHhsaW0oMCwzMCkgKw0KICB5bGltKDAsMzApICsNCiAgY29vcmRfZXF1YWwoKSArDQogIHRoZW1lKHBsb3QudGl0bGUgPSBlbGVtZW50X3RleHQoaGp1c3QgPSAwLjUpLCB0ZXh0ID0gZWxlbWVudF90ZXh0KHNpemU9MjApKQ0KDQpgYGANCg0KIyMgVGFidWxhciBTdGF0aXN0aWNzDQoNCkluIGFkZGl0aW9uIHRvIHRoZSB0YWJ1bGFyIGRhdGEgYWxyZWFkeSBwcm92aWRlZCB3aXRoaW4gdGhlICJQZXJmb3JtYW5jZSBNZXRyaWNzIiBzZWN0aW9uLCBpdCBpcyBhbHNvIHJlY29tbWVuZGVkIHRvIHByb3ZpZGUgdGFidWxhciBkYXRhIG9uIFVwdGltZSAoJSkgYW5kIG51bWJlciBvZiBwYWlyZWQgb2JzZXJ2YXRpb25zIGluIHRoZSBhbmFseXNpcy4gU3BlY2lmaWNhbGx5LCBpdCBpcyByZWNvbW1lbmRlZCB0aGF0IHlvdSByZXBvcnQgdGhlIGZvbGxvd2luZzoNCg0KMS4gUmFuZ2Ugb2YgRlJNL0ZFTSBtb25pdG9yIGNvbmNlbnRyYXRpb25zIG92ZXIgZHVyYXRpb24gb2YgdGVzdCAozrxnL21eM14pDQoyLiBOdW1iZXIgb2YgMjQtaG91ciBwZXJpb2RzIGluIEZSTS9GRU0gbW9uaXRvciBtZWFzdXJlbWVudHMgd2l0aCBhIGdvYWwgY29uY2VudHJhdGlvbiBvZiDiiaUgMjUgzrxnL21eM14NCjMuIE51bWJlciBvZiAyNC1ob3VyIHBlcmlvZHMgb3V0c2lkZSBtYW51ZmFjdHVyZXItbGlzdGVkIHRlbXBlcmF0dXJlIHRhcmdldCBjcml0ZXJpYQ0KNC4gTnVtYmVyIG9mIDI0LWhvdXIgcGVyaW9kcyBvdXRzaWRlIG1hbnVmYWN0dXJlci1saXN0ZWQgcmVsYXRpdmUgaHVtaWRpdHkgdGFyZ2V0IGNyaXRlcmlhDQo1LiBOdW1iZXIgb2YgY29uY3VycmVudGx5IHJlcG9ydGVkIHNlbnNvciBjb25jZW50cmF0aW9uIHZhbHVlcyAoMSBoIGFuZCAyNCBoKQ0KNi4gTnVtYmVyIG9mIHBhaXJlZCBzZW5zb3IgYW5kIEZSTS9GRU0gY29uY2VudHJhdGlvbiB2YWx1ZXMgKDEgaCBhbmQgMjQgaCkNCjcuIE51bWJlciBvZiBwYWlyZWQgMjQtaG91ciwgbm9ybWFsaXplZCBjb25jZW50cmF0aW9uIGFuZCB0ZW1wZXJhdHVyZSB2YWx1ZXMNCjguIE51bWJlciBvZiBwYWlyZWQgMjQtaG91ciwgbm9ybWFsaXplZCBjb25jZW50cmF0aW9uIGFuZCByZWxhdGl2ZSBodW1pZGl0eSB2YWx1ZXMNCjkuIFNlbnNvciB1cHRpbWUgKCUpICgxIGggYW5kIDI0IGgpDQoNCk1hbnkgb2YgdGhlc2UgY2FsY3VsYXRpb25zIGFyZSBzaW1wbGUgdG8gZG8gd2l0aCB0aGUgImNvbXBsZXRlLmNhc2VzIiBmdW5jdGlvbiBpbiBSLiANCg0KTm90ZSB0aGF0IGZvciBpdGVtcyAzLTQgKCMgb2YgcGVyaW9kcyBvdXRzaWRlIG1hbnVmYWN0dXJlci1saXN0ZWQgdGVtcGVyYXR1cmUgYW5kIHJlbGF0aXZlIGh1bWlkaXR5IHRhcmdldCBjcml0ZXJpcykgZm9yIFB1cnBsZUFpciBtb25pdG9ycywgYWNjb3JkaW5nIHRvIHRoZSBtYW51ZmFjdHVyZXIsIGVhY2ggc2Vuc29yIHdpbGwgd29yayBlZmZlY3RpdmVseSBpbiBhIHRlbXBlcmF0dXJlIHJhbmdlIG9mIC0xMC02MMKwQyBhbmQgUkggcmFuZ2Ugb2YgMOKAkzk5ICUgW0BZb25nMjAxNl0uDQoNCkxldCdzIHN0YXJ0IHdpdGggdGhlIGNhbGN1bGF0aW9ucy4gVGhlcmUgYXJlIG1hbnkgbGluZXMgb2YgY29kZSBoZXJlLCBidXQgbW9zdCBhcmUgc2ltcGxlIHN0ZXBzLg0KDQpgYGB7ciB0YWJ1bGFyfQ0KDQojIyAxLiBSYW5nZSBvZiBGUk0vRkVNIG1vbml0b3IgY29uY2VudHJhdGlvbnMgb3ZlciBkdXJhdGlvbiBvZiB0ZXN0ICjOvGcvbV4zXikNCmRmXzFoICU+JQ0KICBncm91cF9ieShMb2NhdGlvbikgJT4lDQogIHN1bW1hcml6ZShNaW49bWluKGRhdGFfMWgsbmEucm09VCksDQogICAgICAgICAgICBNYXg9bWF4KGRhdGFfMWgsbmEucm09VCkpICU+JQ0KICBrYmwoY2FwdGlvbj0iKipQTX4yLjV+IENvbmNlbnRyYXRpb24gUmFuZ2UqKiIpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKQ0KDQojIyAyLiBOdW1iZXIgb2YgMjQtaG91ciBwZXJpb2RzIGluIEZSTS9GRU0gbW9uaXRvciBtZWFzdXJlbWVudHMgd2l0aCBhIGdvYWwgY29uY2VudHJhdGlvbiBvZiDiiaUgMjUgzrxnL21eM14NCmRmXzI0aCAlPiUNCiAgZmlsdGVyKGRhdGFfMjRoID4gMjUpICU+JQ0KICAgIGNvdW50KExvY2F0aW9uKSAlPiUNCiAga2JsKGNhcHRpb249IioqIyAyNCBoIEF2ZXJhZ2UgRGF0YSBQb2ludHMgQWJvdmUgMjUgwrVnL21eM14qKiIpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKQ0KDQojIyAzLiBOdW1iZXIgb2YgMjQtaG91ciBwZXJpb2RzIG91dHNpZGUgbWFudWZhY3R1cmVyLWxpc3RlZCB0ZW1wZXJhdHVyZSB0YXJnZXQgY3JpdGVyaWENCg0KIyBIZXJlIHdlIHVzZSAnY3V0JyB0byBhZGQgYnJlYWtzIHRvIHRoZSBkYXRhIGJhc2VkIG9uIG91ciBrbm93biBndWlkZWxpbmVzICgtMTAgdG8gNjApDQoNCmRmXzI0aF9hbGwgJT4lIG11dGF0ZSgNCiAgcmFuZ2UgPSBjdXQodGVtcCwgYnJlYWtzID0gYygtSW5mLC0xMCwgNjAsIEluZikpKSAlPiUNCiAgZ3JvdXBfYnkocmFuZ2UpICU+JQ0KICBzdW1tYXJpemUoY291bnQgPSBuKCkpICU+JQ0KICBrYmwoY2FwdGlvbj0iKipSYW5nZSBvZiAyNCBoIFQgUmVhZGluZ3MgKG5vbmUgb3V0c2lkZSBvZiByYW5nZSkqKiIpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKQ0KDQojIyA0LiBOdW1iZXIgb2YgMjQtaG91ciBwZXJpb2RzIG91dHNpZGUgbWFudWZhY3R1cmVyLWxpc3RlZCByZWxhdGl2ZSBodW1pZGl0eSB0YXJnZXQgY3JpdGVyaWENCg0KIyBIZXJlIHdlIHVzZSAnY3V0JyB0byBhZGQgYnJlYWtzIHRvIHRoZSBkYXRhIGJhc2VkIG9uIG91ciBrbm93biBndWlkZWxpbmVzICgwIHRvIDk5KQ0KDQpkZl8yNGhfYWxsICU+JSBtdXRhdGUoDQogIHJhbmdlID0gY3V0KHJlbF9odW0sIGJyZWFrcyA9IGMoMCwgOTksIEluZikpKSAlPiUNCiAgZ3JvdXBfYnkocmFuZ2UpICU+JQ0KICBzdW1tYXJpemUoY291bnQgPSBuKCkpICU+JQ0KICBrYmwoY2FwdGlvbj0iKipSYW5nZSBvZiAyNCBoIFJIIFJlYWRpbmdzIChub25lIG91dHNpZGUgb2YgcmFuZ2UpKioiKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCg0KIyMgNS4gTnVtYmVyIG9mIGNvbmN1cnJlbnRseSByZXBvcnRlZCBzZW5zb3IgY29uY2VudHJhdGlvbiB2YWx1ZXMgKDEgaCBhbmQgMjQgaCkgJg0KIyMgNi4gTnVtYmVyIG9mIHBhaXJlZCBzZW5zb3IgYW5kIEZSTS9GRU0gY29uY2VudHJhdGlvbiB2YWx1ZXMgKDEgaCBhbmQgMjQgaCkNCg0KIyBXZSB3aWxsIHRhYnVsYXRlIGl0ZW1zIDUtNiBzaW11bHRhbmVvdXNseS4gTm90ZSB0aGlzIHNlY3Rpb24gb2YgY29kZSBpcyBhIGxpdHRsZSBjbHVua3kgLSBhcG9sb2dpZXMhDQoNCiMgR2V0IHBhaXJlZCBudW1iZXIgb2Ygb2JzZXJ2YXRpb25zIGZvciAxIGggYW5kIDI0IGggYXZlcmFnZXMgdXNpbmcgImNvbXBsZXRlLmNhc2VzIg0KcGFpcmVkXzFoX0IxLkZFTSA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGF0YV8xaF9CdXJuYWJ5MSxkYXRhXzFoX0ZFTSkpDQpwYWlyZWRfMWhfQjIuRkVNIDwtIHN1bShjb21wbGV0ZS5jYXNlcyhkYXRhXzFoX0J1cm5hYnkyLGRhdGFfMWhfRkVNKSkNCnBhaXJlZF8xaF9CMy5GRU0gPC0gc3VtKGNvbXBsZXRlLmNhc2VzKGRhdGFfMWhfQnVybmFieTMsZGF0YV8xaF9GRU0pKQ0KDQpwYWlyZWRfMWhfQjEuQjIgPC0gc3VtKGNvbXBsZXRlLmNhc2VzKGRhdGFfMWhfQnVybmFieTEsZGF0YV8xaF9CdXJuYWJ5MikpDQpwYWlyZWRfMWhfQjEuQjMgPC0gc3VtKGNvbXBsZXRlLmNhc2VzKGRhdGFfMWhfQnVybmFieTEsZGF0YV8xaF9CdXJuYWJ5MykpDQpwYWlyZWRfMWhfQjIuQjMgPC0gc3VtKGNvbXBsZXRlLmNhc2VzKGRhdGFfMWhfQnVybmFieTIsZGF0YV8xaF9CdXJuYWJ5MykpDQoNCnBhaXJlZF8yNGhfQjEuRkVNIDwtIHN1bShjb21wbGV0ZS5jYXNlcyhkYXRhXzI0aF9CdXJuYWJ5MSxkYXRhXzI0aF9GRU0pKQ0KcGFpcmVkXzI0aF9CMi5GRU0gPC0gc3VtKGNvbXBsZXRlLmNhc2VzKGRhdGFfMjRoX0J1cm5hYnkyLGRhdGFfMjRoX0ZFTSkpDQpwYWlyZWRfMjRoX0IzLkZFTSA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGF0YV8yNGhfQnVybmFieTMsZGF0YV8yNGhfRkVNKSkNCg0KcGFpcmVkXzI0aF9CMS5CMiA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGF0YV8yNGhfQnVybmFieTEsZGF0YV8yNGhfQnVybmFieTIpKQ0KcGFpcmVkXzI0aF9CMS5CMyA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGF0YV8yNGhfQnVybmFieTEsZGF0YV8yNGhfQnVybmFieTMpKQ0KcGFpcmVkXzI0aF9CMi5CMyA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGF0YV8yNGhfQnVybmFieTIsZGF0YV8yNGhfQnVybmFieTMpKQ0KDQojIFN0b3JlIGRhdGEgaW4gYSBuZXcgZGF0YSBmcmFtZSBmb3IgcmVwb3J0aW5nDQpwYWlyc19uYW1lcyA8LSBjKCJGRU0iLCJCdXJuYWJ5MSIsIkJ1cm5hYnkyIiwiQnVybmFieTMiKQ0KcGFpcnNfMWhfRkVNIDwtIGMoTkEscGFpcmVkXzFoX0IxLkZFTSxwYWlyZWRfMWhfQjIuRkVNLHBhaXJlZF8xaF9CMy5GRU0pDQpwYWlyc18xaF9CdXJuYWJ5MSA8LSBjKHBhaXJlZF8xaF9CMS5GRU0sTkEscGFpcmVkXzFoX0IxLkIyLHBhaXJlZF8xaF9CMS5CMykNCnBhaXJzXzFoX0J1cm5hYnkyIDwtIGMocGFpcmVkXzFoX0IyLkZFTSxwYWlyZWRfMWhfQjEuQjIsTkEscGFpcmVkXzFoX0IyLkIzKQ0KcGFpcnNfMWhfQnVybmFieTMgPC0gYyhwYWlyZWRfMWhfQjMuRkVNLHBhaXJlZF8xaF9CMS5CMyxwYWlyZWRfMWhfQjIuQjMsTkEpDQpwYWlyc18xaCA8LSBkYXRhLmZyYW1lKHBhaXJzX25hbWVzLHBhaXJzXzFoX0ZFTSxwYWlyc18xaF9CdXJuYWJ5MSxwYWlyc18xaF9CdXJuYWJ5MixwYWlyc18xaF9CdXJuYWJ5MykNCm5hbWVzKHBhaXJzXzFoKSA8LSBjKCJNb25pdG9yIiwiRkVNLjFoIiwiQnVybmFieTEuMWgiLCJCdXJuYWJ5Mi4xaCIsIkJ1cm5hYnkzLjFoIikNCg0KcGFpcnNfMjRoX0ZFTSA8LSBjKE5BLHBhaXJlZF8yNGhfQjEuRkVNLHBhaXJlZF8yNGhfQjIuRkVNLHBhaXJlZF8yNGhfQjMuRkVNKQ0KcGFpcnNfMjRoX0J1cm5hYnkxIDwtIGMocGFpcmVkXzI0aF9CMS5GRU0sTkEscGFpcmVkXzI0aF9CMS5CMixwYWlyZWRfMjRoX0IxLkIzKQ0KcGFpcnNfMjRoX0J1cm5hYnkyIDwtIGMocGFpcmVkXzI0aF9CMi5GRU0scGFpcmVkXzI0aF9CMS5CMixOQSxwYWlyZWRfMjRoX0IyLkIzKQ0KcGFpcnNfMjRoX0J1cm5hYnkzIDwtIGMocGFpcmVkXzI0aF9CMy5GRU0scGFpcmVkXzI0aF9CMS5CMyxwYWlyZWRfMjRoX0IyLkIzLE5BKQ0KcGFpcnNfMjRoIDwtIGRhdGEuZnJhbWUocGFpcnNfbmFtZXMscGFpcnNfMjRoX0ZFTSxwYWlyc18yNGhfQnVybmFieTEscGFpcnNfMjRoX0J1cm5hYnkyLHBhaXJzXzI0aF9CdXJuYWJ5MykNCm5hbWVzKHBhaXJzXzI0aCkgPC0gYygiTW9uaXRvciIsIkZFTS4yNGgiLCJCdXJuYWJ5MS4yNGgiLCJCdXJuYWJ5Mi4yNGgiLCJCdXJuYWJ5My4yNGgiKQ0KDQpwYWlyc19jb21iaW5lZCA8LSBmdWxsX2pvaW4ocGFpcnNfMWgscGFpcnNfMjRoLGJ5PSJNb25pdG9yIikNCg0KIyBQcmludCB0aGUgcmVzdWx0IGluIGEgdGFibGUNCnBhaXJzX2NvbWJpbmVkICU+JQ0KICBrYmwoY2FwdGlvbj0iKiojIFBhaXJlZCBTZW5zb3ItU2Vuc29yIGFuZCBTZW5zb3ItRkVNIE1lYXN1cmVtZW50cyoqIixjb2wubmFtZXM9YygiIiwiRkVNIiwiQnVybmFieTEiLCJCdXJuYWJ5MiIsIkJ1cm5hYnkzIiwiRkVNIiwiQnVybmFieTEiLCJCdXJuYWJ5MiIsIkJ1cm5hYnkzIikpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKSAlPiUNCiAgYWRkX2hlYWRlcl9hYm92ZShjKCIgIiA9IDEsICIxIGgiID0gNCwgIjI0IGgiID0gNCkpDQoNCiMjIDcuIE51bWJlciBvZiBwYWlyZWQgMjQtaG91ciwgbm9ybWFsaXplZCBjb25jZW50cmF0aW9uIGFuZCB0ZW1wZXJhdHVyZSB2YWx1ZXMgJg0KIyMgOC4gTnVtYmVyIG9mIHBhaXJlZCAyNC1ob3VyLCBub3JtYWxpemVkIGNvbmNlbnRyYXRpb24gYW5kIHJlbGF0aXZlIGh1bWlkaXR5IHZhbHVlcw0KDQojIFdlIHdpbGwgdGFidWxhdGUgaXRlbXMgNy04IHNpbXVsdGFuZW91c2x5LiBOb3RlIHRoaXMgc2VjdGlvbiBvZiBjb2RlIGlzIGEgbGl0dGxlIGNsdW5reSAtIGFwb2xvZ2llcyENCg0KcGFpcnNfbWV0X25hbWVzIDwtIGMoIkJ1cm5hYnkxIiwiQnVybmFieTIiLCJCdXJuYWJ5MyIpDQoNCiMgR2V0IHBhaXJlZCBudW1iZXIgb2YgMjQgaCBtZWFzdXJlbWVudHMgYXZlcmFnZXMgdXNpbmcgImNvbXBsZXRlLmNhc2VzIg0KcGFpcmVkXzI0aF9uQjEuVCA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGZfMjRoX2FsbCRub3JtX0J1cm5hYnkxLGRmXzI0aF9hbGwkdGVtcCkpDQpwYWlyZWRfMjRoX25CMi5UIDwtIHN1bShjb21wbGV0ZS5jYXNlcyhkZl8yNGhfYWxsJG5vcm1fQnVybmFieTIsZGZfMjRoX2FsbCR0ZW1wKSkNCnBhaXJlZF8yNGhfbkIzLlQgPC0gc3VtKGNvbXBsZXRlLmNhc2VzKGRmXzI0aF9hbGwkbm9ybV9CdXJuYWJ5MyxkZl8yNGhfYWxsJHRlbXApKQ0KDQpwYWlyZWRfMjRoX25CMS5SSCA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGZfMjRoX2FsbCRub3JtX0J1cm5hYnkxLGRmXzI0aF9hbGwkcmVsX2h1bSkpDQpwYWlyZWRfMjRoX25CMi5SSCA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGZfMjRoX2FsbCRub3JtX0J1cm5hYnkyLGRmXzI0aF9hbGwkcmVsX2h1bSkpDQpwYWlyZWRfMjRoX25CMy5SSCA8LSBzdW0oY29tcGxldGUuY2FzZXMoZGZfMjRoX2FsbCRub3JtX0J1cm5hYnkzLGRmXzI0aF9hbGwkcmVsX2h1bSkpDQoNCiMgU3RvcmUgdGhlIGRhdGEgYW5kIGNvbWJpbmUgaW4gYSBuZXcgZGF0YSBmcmFtZQ0KcGFpcnNfVCA8LSBjKHBhaXJlZF8yNGhfbkIxLlQscGFpcmVkXzI0aF9uQjIuVCxwYWlyZWRfMjRoX25CMy5UKQ0KcGFpcnNfUkggPC0gYyhwYWlyZWRfMjRoX25CMS5SSCxwYWlyZWRfMjRoX25CMi5SSCxwYWlyZWRfMjRoX25CMy5SSCkNCg0KcGFpcnNfbWV0IDwtIGRhdGEuZnJhbWUocGFpcnNfbWV0X25hbWVzLHBhaXJzX1QscGFpcnNfUkgpDQoNCiMgUHJpbnQgdGhlIHJlc3VsdC4NCnBhaXJzX21ldCAlPiUNCiAga2JsKGNhcHRpb249IioqIyAyNCBoIEF2Zy4gUGFpcmVkIE5vcm1hbGl6ZWQgQ29uY2VudHJhdGlvbiBhbmQgTWV0IE1lYXN1cmVtZW50cyoqIixjb2wubmFtZXM9YygiU2Vuc29yIiwiVCIsIlJIIikpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKQ0KDQoNCiMjIDkuIFNlbnNvciB1cHRpbWUgKCUpICgxIGggYW5kIDI0IGgpDQoNCiMgRGV0ZXJtaW5lIHRoZSBtYXhpbXVtIG51bWJlciBvZiBvYnNlcnZhdGlvbnMNCm1heF8xaCA8LSBucm93KGRmXzFoX3ByZWNpc2lvbikNCm1heF8yNGggPC0gbnJvdyhkZl8yNGhfcHJlY2lzaW9uKQ0KDQojIENhbGN1bGF0ZSB1cHRpbWUgYnkgYWRkaW5nIHVwIHJvd3MgdGhhdCBhcmUgbm90ICdOQScgYW5kIGRpdmlkaW5nIGJ5IG1heC4gIyBvZiBvYnNlcnZhdGlvbnMNCnVwdGltZS4xaC5CMSA8LSBzdW0oIWlzLm5hKGRhdGFfMWhfQnVybmFieTEpKSAvIG1heF8xaCAqIDEwMA0KdXB0aW1lLjFoLkIyIDwtIHN1bSghaXMubmEoZGF0YV8xaF9CdXJuYWJ5MikpIC8gbWF4XzFoICogMTAwDQp1cHRpbWUuMWguQjMgPC0gc3VtKCFpcy5uYShkYXRhXzFoX0J1cm5hYnkzKSkgLyBtYXhfMWggKiAxMDANCnVwdGltZS4yNGguQjEgPC0gc3VtKCFpcy5uYShkYXRhXzI0aF9CdXJuYWJ5MSkpIC8gbWF4XzI0aCAqIDEwMA0KdXB0aW1lLjI0aC5CMiA8LSBzdW0oIWlzLm5hKGRhdGFfMjRoX0J1cm5hYnkyKSkgLyBtYXhfMjRoICogMTAwDQp1cHRpbWUuMjRoLkIzIDwtIHN1bSghaXMubmEoZGF0YV8yNGhfQnVybmFieTMpKSAvIG1heF8yNGggKiAxMDANCg0KIyBTdG9yZSB0aGUgZGF0YSBhbmQgY29tYmluZSBpbiBhIG5ldyBkYXRhIGZyYW1lDQp1cHRpbWVfMWggPC0gYyh1cHRpbWUuMWguQjEsdXB0aW1lLjFoLkIyLHVwdGltZS4xaC5CMykNCnVwdGltZV8yNGggPC0gYyh1cHRpbWUuMjRoLkIxLHVwdGltZS4yNGguQjIsdXB0aW1lLjI0aC5CMykNCg0KdXB0aW1lIDwtIGRhdGEuZnJhbWUocGFpcnNfbWV0X25hbWVzLHVwdGltZV8xaCx1cHRpbWVfMjRoKQ0KDQojIFByaW50IHRoZSByZXN1bHQuDQp1cHRpbWUgJT4lIA0KICBrYmwoY2FwdGlvbj0iKipTZW5zb3IgVXB0aW1lICglKSoqIixjb2wubmFtZXMgPSBjKCJTZW5zb3IiLCIxIGgiLCIyNCBoIikpICU+JQ0KICBrYWJsZV9zdHlsaW5nKGJvb3RzdHJhcF9vcHRpb25zID0gYygic3RyaXBlZCIsICJob3ZlciIpKQ0KYGBgDQoNCg0KV2UgaGF2ZSBub3cgY29tcGxldGVkIGFsbCBvZiB0aGUgbWFpbiByZXBvcnRpbmcgbWV0cmljcyByZXF1ZXN0ZWQgaW4gdGhlIFUuUy4gRVBBIEJhc2UgVGVzdGluZyBQTX4yLjV+IFJlcG9ydC4gRnJvbSB0aGlzIGFuYWx5c2lzLCB3ZSBjYW4gY29uY2x1ZGUgdGhhdCB3aXRob3V0IGNhbGlicmF0aW9uLCB0aGVzZSBzZW5zb3JzIGZhaWwgdG8gbWVldCBzb21lIHRhcmdldHMgKFJNU0UsIE5STVNFLCBTRCksIGJ1dCBpbiBnZW5lcmFsIGFyZSB3ZWxsLWNvcnJlbGF0ZWQgdG8gYm90aCB0aGUgcmVndWxhdG9yeSBtb25pdG9yaW5nIGRhdGEgYW5kIGVhY2ggb3RoZXIuIExvb2tpbmcgYXQgdGhlIGRhdGEsIHRoaXMgd2FzIGxpa2VseSBza2V3ZWQgYnkgdGhlIHBvb3JlciBwZXJmb3JtYW5jZSBvZiAiQnVybmFieTIiIGFuZCB0aGUgd2lsZGZpcmUgcGVyaW9kIChpbiBmYWN0IHRoZSBVLlMuIEVQQSBjYXV0aW9ucyBhYm91dCB2ZXJ5IGhpZ2ggY29uY2VudHJhdGlvbiBwZXJpb2RzIGluIHlvdXIgZGF0YSBzZXQpLg0KVGhpcyBpcyBzdW1tYXJpemVkIGJyaWVmbHkgYmVsb3c6DQoNCiogQWNjdXJhY3k6IA0KICArIFJeMl46IFBhc3MuIFRhcmdldCBpcyA+IDAuNyBhbmQgdGhlIG1lYW4gYWNyb3NzIGFsbCBzZW5zb3JzIHdhcyAwLjg0ICgxIGgpIGFuZCAwLjkzICgyNCBoKQ0KICArIFNsb3BlOiBQYXNzLiBUYXJnZXQgaXMgMSArLy0gMC4zNSBhbmQgdGhlIG1lYW4gYWNyb3NzIGFsbCBzZW5zb3JzIHdhcyAxLjAzICgxIGggKSBhbmQgMC45OCAoMjQgaCkNCiAgKyBJbnRlcmNlcHQ6IFBhc3MuIFRhcmdldCBpcyAtNSB0byArNSBhbmQgdGhlIG1lYW4gYWNyb3NzIGFsbCBzZW5zb3JzIHdhcyAzLjE2IM68Zy9tXjNeICgxIGggKSBhbmQgMy40MyDOvGcvbV4zXiAoMjQgaCkNCiAgKyBSTVNFOiBGYWlsLiBUYXJnZXQgaXMg4omkIDcgzrxnL21eM14gYW5kIHRoZSBtZWFuIGFjcm9zcyBhbGwgc2Vuc29ycyB3YXMgMTIuMDIgzrxnL21eM14gKDEgaCApIGFuZCA4LjQ3IM68Zy9tXjNeICgyNCBoKS4gVGhpcyB3YXMgaW5mbHVlbmNlZCBoZWF2aWx5IGJ5IHNlbnNvciAiQnVybmFieTIiIHdoaWNoIHNlZW1zIHRvIGhhdmUgc29tZSBpc3N1ZXMuDQogICsgTlJNU0U6IEZhaWwuIFRhcmdldCBpcyDiiaQgMzAlIGFuZCB0aGUgbWVhbiBhY3Jvc3MgYWxsIHNlbnNvcnMgd2FzIDExNy44NSUgKDEgaCApIGFuZCA3NC42NiUgKDI0IGgpLiBUaGlzIHdhcyBsaWtlbHkgaW5mbHVlbmNlZCBieSB0aGUgd2lsZGZpcmUgcGVyaW9kLiANCiogUHJlY2lzaW9uOg0KICArIFNEOiBGYWlsZWQuIFRhcmdldCBpcyA8NSDOvGcvbV4zXiBhbmQgdGhlIG1lYW4gYWNyb3NzIGFsbCBzZW5zb3JzIHdhcyA1MS42NyDOvGcvbV4zXiAoMSBoICkgYW5kIDE4LjQyIM68Zy9tXjNeICgyNCBoKS4gVGhpcyB3YXMgbGlrZWx5IHNrZXdlZCBieSB0aGUgcG9vcmVyIHBlcmZvcm1hbmNlIG9mICJCdXJuYWJ5MiIgYW5kIHRoZSBoaWdoIGNvbmNlbnRyYXRpb25zIGR1cmluZyB0aGUgd2lsZGZpcmUgcGVyaW9kLg0KICArIENWKCUpOiBQYXNzLiBUYXJnZXQgaXMgPDMwJS4gRm9yIGJvdGggMSBoIGFuZCAyNCBoIGNhbGN1bGF0aW9ucyB0aGlzIHdhcyA8IDEwJS4NCiogUmVtYWluaW5nIFRhYnVsYXIgU3RhdGlzdGljczogQWxsIHBhc3MuIEV4Y2VwdGlvbmFsIHVwdGltZS4NCg0KDQojIFBhcnQgMzogQnVpbGRpbmcgQ2FsaWJyYXRpb24gTW9kZWxzDQoNClRoZSBsYXN0IHRoaW5nIHdlIHdpbGwgY29uc2lkZXIgaXMgYnVpbGRpbmcgYSBjYWxpYnJhdGlvbiBtb2RlbC4gTW9zdCBsb3ctY29zdCBQTSBzZW5zb3JzIGhhdmUgcmVsaWVkIG9uIG11bHRpcGxlIGxpbmVhciByZWdyZXNzaW9uIHdpdGggcGFyYW1ldGVycyBzdWNoIGFzIHRlbXBlcmF0dXJlLCByZWxhdGl2ZSBodW1pZGl0eSBhbmQgZGV3IHBvaW50LiBNYW55IGxvdy1jb3N0IHNlbnNvcnMgbWVhc3VyZSB0ZW1wZXJhdHVyZSBhbmQgcmVsYXRpdmUgaHVtaWRpdHksIGhvd2V2ZXIsIHRoZXNlIGFyZSBub3QgYWx3YXlzIHJlbGlhYmxlIChpZiB0aGV5IGFyZSBhZmZlY3RlZCBieSBpbnRlcm5hbCBoZWF0aW5nIG9yIGNvbmRpdGlvbnMgd2l0aGluIHRoZSBzZW5zb3IpLCBhbmQgc28gbW9kZWxzIHNob3VsZCBiZSBidWlsdCB1c2luZyBleHRlcm5hbCBtZXQgc3RhdGlvbnMgaWYgcG9zc2libGUuIExldCdzIGNvbnNpZGVyIGJ1aWxkaW5nIGEgY2FsaWJyYXRpb24gbW9kZWwgZm9yIHNlbnNvciAiQnVybmFieTEiIGF0IDEgaG91ciB0aW1lIHJlc29sdXRpb24gYW5kIGxvb2sgYXQgc29tZSB0ZWNobmlxdWVzIGZvciBhc3Nlc3NpbmcgbW9kZWwgcGVyZm9ybWFuY2UuDQoNClRoaXMgaXMgbW9zdGx5IGZvciBkZW1vbnN0cmF0aW9uIHB1cnBvc2VzIC0gdGhlIHJhdyBwZXJmb3JtYW5jZSBvZiBCdXJuYWJ5MSB3YXMgcmVhc29uYWJseSBnb29kIGJhc2VkIG9uIFBhcnQgMi4gSG93ZXZlciwgaWxsdXN0cmF0aW5nIHNvbWUgYXBwcm9hY2hlcyBpcyBzdGlsbCB1c2VmdWwuIExldCdzIGJlZ2luLg0KDQojIyBQcmVwYXJlIHRoZSBkYXRhDQoNCkJlZm9yZSB3ZSBiZWdpbiwgd2Ugd2lsbCBuZWVkIHRvIGV4dHJhY3QgdGhlIHJlbGV2YW50IGRhdGEgZm9yIG1vZGVsIGJ1aWxkaW5nIGFuZCBzZXBhcmF0ZSBpdCBpbnRvIHRlc3RpbmcgYW5kIHRyYWluaW5nIGRhdGEgc2V0cy4gQSB0eXBpY2FsIHRlc3RpbmctdHJhaW5pbmcgc3BsaXQgaXMgODA6MjAuIFdlIHdpbGwgdXNlIHRoZSBjYXJldCBwYWNrYWdlIHRvIGFjaGlldmUgdGhpcyBzcGxpdHRpbmcgW0BSLWNhcmV0XS4NCg0KYGBge3IgY2FsX3ByZXB9DQoNCiMgV2UgYWxyZWFkeSBkb3dubG9hZGVkIHRoZSBob3VybHkgd2VhdGhlciBkYXRhLCB3ZSBuZWVkIHRvIG5vdyBqb2luIGl0IHRvIHRoZSAxIGggZGF0YS4NCg0Kd2VhdGhlcl8xaCA8LSByZW5hbWUod2VhdGhlcl8xaCxkYXRldGltZV8xaD1kYXRlKQ0KZGZfY2FsaWJyYXRpb24gPC0gZnVsbF9qb2luKGRmXzFoX3ByZWNpc2lvbix3ZWF0aGVyXzFoLGJ5PSJkYXRldGltZV8xaCIpDQoNCiMgTGV0J3Mgc2VsZWN0IHRoZSBjb2x1bW5zIHdlIGFyZSBpbnRlcmVzdGVkIGluIGluY2x1ZGluZyBpbiBvdXIgbXVsdGlwbGUgbGluZWFyIHJlZ3Jlc3Npb24gbW9kZWwNCg0KZGZfY2FsaWJyYXRpb24gPC0gZGZfY2FsaWJyYXRpb24gJT4lIHNlbGVjdChGRU0sQnVybmFieTEsdGVtcCxyZWxfaHVtLHRlbXBfZGV3KQ0KDQojIFdlIHdpbGwgb25seSBrZWVwIGNvbXBsZXRlIGNhc2VzIGZvciBtb2RlbCBidWlsZGluZyAoZXZlcnkgY29sdW1uIGhhcyBhIG1lYXN1cmVtZW50LikNCg0KZGZfY2FsaWJyYXRpb24gPC0gZGZfY2FsaWJyYXRpb25bY29tcGxldGUuY2FzZXMoZGZfY2FsaWJyYXRpb24pLF0NCg0KIyBNYWtlIGFuIGluZGV4IGZvciBzcGxpdHRpbmcgZGF0YSBpbnRvIHRyYWluaW5nIGFuZCB0ZXN0aW5nDQoNCmRhdGFfc3BsaXQgPC0gY3JlYXRlRGF0YVBhcnRpdGlvbihkZl9jYWxpYnJhdGlvbiRGRU0sIHRpbWVzPTEsIGxpc3Q9RixwPTAuOCkNCmRhdGFfdHJhaW4gPC0gZGZfY2FsaWJyYXRpb25bZGF0YV9zcGxpdCxdDQpkYXRhX3Rlc3QgPC0gZGZfY2FsaWJyYXRpb25bLWRhdGFfc3BsaXQsXQ0KDQpgYGANCg0KIyMgQnVpbGQgc2FtcGxlIG1vZGVscw0KDQpBcyBhIGRlbW9uc3RyYXRpb24sIHdlIHdpbGwgY29uc2lkZXIgZXZlcnkgbGluZWFyIGNvbWJpbmF0aW9uIG9mIHRoZSBzZW5zb3Igc2lnbmFsLCB0aGUgcmVsYXRpdmUgaHVtaWRpdHkgYW5kIHRoZSBkZXcgcG9pbnQgaW4gb3VyIG1vZGVsIGJ1aWxkaW5nLiBXZSBhcmUgZXhjbHVkaW5nIHRlbXBlcmF0dXJlIGFzIGl0IHdhcyBwcmV2aW91c2x5IHNob3duIHRvIGhhdmUgbGltaXRlZCBpbmZsdWVuY2UgKFBhcnQgMiwgTWV0ZW9yb2xvaWdjYWwgaW5mbHVlbmNlKS4gR2l2ZW4gdGhlIHJlc3VsdHMgb2Ygc2VwYXJhdGluZyB0aGUgaGlnaCBjb25jZW50cmF0aW9uIGFuZCBsb3cgY29uY2VudHJhdGlvbiBhbmQgdGhlIGNoYW5nZSBpbiBzbG9wZSAoUGFydCAyLCBQZXJmb3JtYW5jZSBNZXRyaWNzKSwgd2Ugd2lsbCBhbHNvIHRyeSB0aHJlZSBwaWVjZXdpc2UgcmVncmVzc2lvbiBtb2RlbHMuIFdlIHdpbGwgdGFrZSBhZHZhbnRhZ2Ugb2YgdGhlICJzZWdtZW50ZWQiIHBhY2thZ2UgdG8gZGV0ZXJtaW5lIHRoZSBzcGxpdCBwb2ludCBpbiBQdXJwbGVBaXIgbW9uaXRvciBjb25jZW50cmF0aW9uIFtAUi1zZWdtZW50ZWQ7QE11Z2dlbzIwMDNdLiBXZSB3aWxsIGR1cGxpY2F0ZSBtb2RlbHMgQSwgQiBhbmQgSCB1c2luZyB0aGUgcGllY2V3aXNlIHJlZ3Jlc3Npb24gYXBwcm9hY2guDQoNCkEgbm90ZTogVGhlcmUgYXJlIG51bWVyb3VzIG9wdGlvbnMgZm9yIGNhbGlicmF0aW9uLCBidXQgdGhpcyB3aWxsIG5vdCBiZSB0aGUgZm9jdXMgb2YgdGhlIHR1dG9yaWFsLiBUaGUgcHJpbWFyeSBnb2FsIGlzIHRvIGlsbHVzdHJhdGUgaG93IG1vZGVsIHBlcmZvcm1hbmNlIGNhbiBiZSBhc3Nlc3NlZC4gQXMgc3VjaCwgZG8gbm90IGNvbnNpZGVyIHRoZXNlIG1vZGVscyBhcyB5b3VyIG9ubHkgb3B0aW9uLCBqdXN0IHNvbWUgZXhhbXBsZSBtb2RlbHMgdG8gbG9vayBhdCBwZXJmb3JtYW5jZSBtZXRyaWNzLiANCg0KYGBge3IgbGluZWFyX21vZGVsc30NCiMgTW9kZWwgQTogSW5jbHVkaW5nIGFsbCBwYXJhbWV0ZXJzIA0KbG1fbW9kZWwuQSA8LSBsbShGRU0gfiBCdXJuYWJ5MSArIHJlbF9odW0gKyB0ZW1wX2RldywgZGF0YSA9IGRhdGFfdHJhaW4pDQoNCiMgTW9kZWwgQjogRXhjbHVkZSBkZXcgcG9pbnQNCmxtX21vZGVsLkIgPC0gbG0oRkVNIH4gQnVybmFieTEgKyByZWxfaHVtLCBkYXRhID0gZGF0YV90cmFpbikNCg0KIyBNb2RlbCBDOiBFeGNsdWRlIHJlbGF0aXZlIGh1bWlkaXR5DQpsbV9tb2RlbC5DIDwtIGxtKEZFTSB+IEJ1cm5hYnkxICsgdGVtcF9kZXcsIGRhdGEgPSBkYXRhX3RyYWluKQ0KDQojIE1vZGVsIEQ6IE9ubHkgaW5jbHVkZSBzZW5zb3IgYXMgcHJlZGljdG9yIG9mIEZFTQ0KbG1fbW9kZWwuRCA8LSBsbShGRU0gfiBCdXJuYWJ5MSwgZGF0YSA9IGRhdGFfdHJhaW4pDQoNCiMjIExldCdzIGFsc28gdHJ5IHNvbWUgc2VnbWVudGVkIChwaWVjZXdpc2UpIG1vZGVscyBiYXNlZCBvbiB3aGF0IHdlIGxlYXJuZWQgaW4gUGFydCAyLiBXZSB3aWxsIHVzZSB0aGUgJ3NlZ21lbnRlZCcgcGFja2FnZSwgYW5kIHVzZSAxIGJyZWFrLg0KDQojIE1vZGVsIEU6IFNlZ21lbnRlZCBtb2RlbCAtIGp1c3Qgc2Vuc29yIGFuZCBGRU0gbW9uaXRvcg0KbG1fbW9kZWwuRSA8LSBzZWdtZW50ZWQobG0oRkVNIH4gQnVybmFieTEsZGF0YSA9IGRhdGFfdHJhaW4pLCBzZWcuWiA9IH5CdXJuYWJ5MSwgcHNpPU5BLCBjb250cm9sID0gc2VnLmNvbnRyb2woSz0xKSkNCg0KIyBNb2RlbCBGOiBTZWdtZW50ZWQgbW9kZWwgLSBTZW5zb3IgKyByZWxhdGl2ZSBodW1pZGl0eQ0KbG1fbW9kZWwuRiA8LSBzZWdtZW50ZWQobG0oRkVNIH4gQnVybmFieTErcmVsX2h1bSxkYXRhID0gZGF0YV90cmFpbiksIHNlZy5aID0gfkJ1cm5hYnkxLCBwc2k9TkEsIGNvbnRyb2wgPSBzZWcuY29udHJvbChLPTEpKQ0KDQojIE1vZGVsIEc6IFNlZ21lbnRlZCBtb2RlbCA9IFNlbnNvciArICByZWxhdGl2ZSBodW1pZHR5ICsgZGV3IHBvaW50DQpsbV9tb2RlbC5HIDwtIHNlZ21lbnRlZChsbShGRU0gfiBCdXJuYWJ5MStyZWxfaHVtK3RlbXBfZGV3LGRhdGEgPSBkYXRhX3RyYWluKSwgc2VnLlogPSB+QnVybmFieTEsIHBzaT1OQSwgY29udHJvbCA9IHNlZy5jb250cm9sKEs9MSkpDQoNCg0KDQojIFByZWRpY3QgdGhlIHRlc3RpbmcgZGF0YSBjb25jZW50cmF0aW9ucyB3aXRoIG91ciBtb2RlbHMNCnByZWRpY3RfbW9kZWwuQSA8LSBwcmVkaWN0KGxtX21vZGVsLkEsZGF0YV90ZXN0KQ0KcHJlZGljdF9tb2RlbC5CIDwtIHByZWRpY3QobG1fbW9kZWwuQixkYXRhX3Rlc3QpDQpwcmVkaWN0X21vZGVsLkMgPC0gcHJlZGljdChsbV9tb2RlbC5DLGRhdGFfdGVzdCkNCnByZWRpY3RfbW9kZWwuRCA8LSBwcmVkaWN0KGxtX21vZGVsLkQsZGF0YV90ZXN0KQ0KcHJlZGljdF9tb2RlbC5FIDwtIHByZWRpY3QobG1fbW9kZWwuRSxkYXRhX3Rlc3QpDQpwcmVkaWN0X21vZGVsLkYgPC0gcHJlZGljdChsbV9tb2RlbC5GLGRhdGFfdGVzdCkNCnByZWRpY3RfbW9kZWwuRyA8LSBwcmVkaWN0KGxtX21vZGVsLkcsZGF0YV90ZXN0KQ0KDQptZWFzdXJlZF90ZXN0IDwtIGRhdGFfdGVzdCAlPiUgcHVsbChGRU0pDQoNCiMgU3RvcmUgdGhlIGRhdGENCmRmX2NhbF9maXQgPC0gZGF0YS5mcmFtZShwcmVkaWN0X21vZGVsLkEscHJlZGljdF9tb2RlbC5CLHByZWRpY3RfbW9kZWwuQyxwcmVkaWN0X21vZGVsLkQscHJlZGljdF9tb2RlbC5FLHByZWRpY3RfbW9kZWwuRixwcmVkaWN0X21vZGVsLkcsbWVhc3VyZWRfdGVzdCkNCmRmX2NhbF9maXRfbG9uZyA8LSBkZl9jYWxfZml0ICU+JSANCiAgcGl2b3RfbG9uZ2VyKGNvbHM9c3RhcnRzX3dpdGgoInByZWRpY3RfIiksbmFtZXNfdG89Im1vZGVsIixuYW1lc19wcmVmaXg9InByZWRpY3RfbW9kZWwuIix2YWx1ZXNfdG89InByZWRpY3RlZF90ZXN0IikNCmRmX2NhbF9maXRfbG9uZyA8LSBzZXREVChkZl9jYWxfZml0X2xvbmcpDQpgYGANCg0KIyMgQXNzZXNzIG1vZGVscw0KDQpXZSB3aWxsIGFzc2VzcyBtb2RlbHMgaW4gMiB3YXlzOiANCg0KKiBUYXlsb3IgRGlhZ3JhbXMgd2hpY2ggcHJvdmlkZSBhIHF1aWNrIHZpc3VhbCBzbmFwc2hvdCBvZiB0aHJlZSBtZXRyaWNzOg0KICArIFN0YW5kYXJkIGRldmlhdGlvbiwgd2hpY2ggaXMgbWVhc3VyZWQgYmFzZWQgb24gcmFkaWFsIGRpc3RhbmNlIHRvIHRoZSBtYXJrZXINCiAgKyBDb3JyZWxhdGlvbiBjb2VmZmljaWVudA0KICArIENlbnRlcmVkIFJNU0UNCiogVGFidWxhciBkYXRhIG9mIFJeMl4gYW5kIHN0YW5kYXJkIGVycm9yDQoNClRoZSBUYXlsb3IgRGlhZ3JhbSBpcyBlYXNpbHkgY29uc3RydWN0ZWQgdXNpbmcgdGhlICJUYXlsb3JEaWFncmFtIiBmdW5jdGlvbiBpbiB0aGUgUiBQYWNrYWdlICJvcGVuYWlyIiBbQFItb3BlbmFpcl0uIFRvIHJlYWQgbW9yZSBhYm91dCBpbnRlcnByZXRpbmcgVGF5bG9yIERpYWdyYW1zIHBsZWFzZSBbc2VlIHRoZSBvcGVuYWlyIGJvb2ssIHNlY3Rpb24gMjFdKGh0dHBzOi8vYm9va2Rvd24ub3JnL2RhdmlkX2NhcnNsYXcvb3BlbmFpci9zZWMtVGF5bG9yRGlhZ3JhbS5odG1sKS4gDQoNCmBgYHtyIG1vZGVsX3BlcmZvcm1hbmNlLCBmaWcuc2hvdz0iaG9sZCIsIG91dC53aWR0aD0iNTAlIn0NCg0KDQpzdW1tYXJ5LkEgPC0gZ2xhbmNlKGxtX21vZGVsLkEpDQpzdW1tYXJ5LkIgPC0gZ2xhbmNlKGxtX21vZGVsLkIpDQpzdW1tYXJ5LkMgPC0gZ2xhbmNlKGxtX21vZGVsLkMpDQpzdW1tYXJ5LkQgPC0gZ2xhbmNlKGxtX21vZGVsLkQpDQpzdW1tYXJ5LkUgPC0gZ2xhbmNlKGxtX21vZGVsLkUpDQpzdW1tYXJ5LkYgPC0gZ2xhbmNlKGxtX21vZGVsLkYpDQpzdW1tYXJ5LkcgPC0gZ2xhbmNlKGxtX21vZGVsLkcpDQoNCnN1bW1hcnlfYWxsX21vZGVscyA8LSBiaW5kX3Jvd3Moc3VtbWFyeS5BLHN1bW1hcnkuQixzdW1tYXJ5LkMsc3VtbWFyeS5ELHN1bW1hcnkuRSxzdW1tYXJ5LkYsc3VtbWFyeS5HKSAgJT4lDQogIG11dGF0ZShtb2RlbElEPWMoIkEiLCJCIiwiQyIsIkQiLCJFIiwiRiIsIkciKSkNCg0KdGF5bG9yIDwtIFRheWxvckRpYWdyYW0oZGZfY2FsX2ZpdF9sb25nLCBvYnMgPSAibWVhc3VyZWRfdGVzdCIsIG1vZCA9ICJwcmVkaWN0ZWRfdGVzdCIsIGdyb3VwID0gIm1vZGVsIiwgbWFpbj0iVGF5bG9yIERpYWdyYW0iKQ0Kem9vbWVkIDwtIFRheWxvckRpYWdyYW0oZGZfY2FsX2ZpdF9sb25nLCBvYnMgPSAibWVhc3VyZWRfdGVzdCIsIG1vZCA9ICJwcmVkaWN0ZWRfdGVzdCIsIGdyb3VwID0gIm1vZGVsIix4bGltPWMoMTksMjMpLHlsaW09Yyg0LDEwKSwgbWFpbj0iWm9vbWVkIEluIikNCg0Kc3VtbWFyeV9hbGxfbW9kZWxzICU+JSBzZWxlY3QobW9kZWxJRCxyLnNxdWFyZWQsc2lnbWEpICU+JQ0KICBrYmwoY2FwdGlvbj0iKipTb21lIE1vZGVsIFBlcmZvcm1hbmNlIE1ldHJpY3MqKiIsY29sLm5hbWVzID0gYygiTW9kZWwgSUQiLCJSXjJeIiwiU3RhbmRhcmQgRXJyb3IiKSxkaWdpdHM9MykgJT4lDQogIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSBjKCJzdHJpcGVkIiwgImhvdmVyIikpDQpgYGANCg0KRnJvbSB0aGlzLCB3ZSBjYW4gc2VlIHRoYXQgZXZlbiB3aXRob3V0IGFueSBjb3JyZWN0aW9ucywgdGhlIFB1cnBsZUFpciBtb25pdG9ycyBhcmUgd2VsbCBjb3JyZWxhdGVkIHRvIHRoZSBGRU0gbW9uaXRvcnMgKG1vZGVsIEQpLiBUaGUgc2VnbWVudGVkIG1vZGVscyAoRS1HKSBoYXZlIG5vdGFibHkgYmV0dGVyIHBlcmZvcm1hbmNlIHRoYW4gdGhlIGxpbmVhciBtb2RlbHMgYWNyb3NzIGFsbCBjb25jZW50cmF0aW9uIHJhbmdlcywgYXMgd2FzIGV4cGVjdGVkIGJhc2VkIG9uIHRoZSBQZXJmb3JtYW5jZSBNZXRyaWNzIGFzc2Vzc21lbnQgc3BsaXQgYWNyb3NzIDIgZGlmZmVyZW50IGNvbmNlbnRyYXRpb24gcmFuZ2VzLg0KDQpUaGUgJ3NlZ21lbnRlZCcgbW9kZWwgYXV0b21hdGljYWxseSBjaG9vc2VzIHRoZSBicmVhayBwb2ludC4gTGV0J3Mgc2VlIHdoYXQgYnJlYWsgcG9pbnQgaXQgY2hvc2UgZm9yIG1vZGVscyBFLUcuDQoNCmBgYHtyIGJyZWFrcG9pbnR9DQoNCmJyZWFrcHQuRSA8LSBzdW1tYXJ5LnNlZ21lbnRlZChsbV9tb2RlbC5FKSRwc2lbMl0NCmJyZWFrcHQuRiA8LSBzdW1tYXJ5LnNlZ21lbnRlZChsbV9tb2RlbC5GKSRwc2lbMl0NCmJyZWFrcHQuRyA8LSBzdW1tYXJ5LnNlZ21lbnRlZChsbV9tb2RlbC5HKSRwc2lbMl0NCmJyZWFrcHRzIDwtIGMoYnJlYWtwdC5FLGJyZWFrcHQuRixicmVha3B0LkcpDQpicmVha3B0c19JRCA8LSBjKCJNb2RlbCBFIiwgIk1vZGVsIEYiLCAiTW9kZWwgRyIpDQpkZl9icmVha3BvaW50cyA8LSBkYXRhLmZyYW1lKGJyZWFrcHRzX0lELGJyZWFrcHRzKSANCmRmX2JyZWFrcG9pbnRzICU+JQ0KICBrYmwoY2FwdGlvbj0iKipTdW1tYXJ5IG9mIGJyZWFrcG9pbnQgbG9jYXRpb24gKM68Zy9tXjNeKSoqIixkaWdpdHM9MiwgY29sLm5hbWVzPWMoIk1vZGVsIElEIiwiQnJlYWtwb2ludCIpKSAlPiUNCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCAiaG92ZXIiKSkNCmBgYA0KDQpBcyB3ZSBjYW4gc2VlLCBpbiBhbGwgbW9kZWxzLCB0aGUgYnJlYWsgcG9pbnQgaXMgdmVyeSBzaW1pbGFyLCBhcm91bmQgMzIgzrxnL21eM14uIExldCdzIHZpc3VhbGl6ZSB0aGlzIGJyZWFrIHBvaW50IGZvciBvdXIgYmVzdCBwZXJmb3JtaW5nIG1vZGVsIChtb2RlbCBGKS4gVGhpcyB3aWxsIGJlIHRoZSBsYXN0IHN0ZXAgaW4gb3VyIHR1dG9yaWFsLg0KDQpgYGB7ciBicmVha3BvaW50X3Bsb3R9DQpkZi5tb2RlbC5GIDwtIGRhdGFfdHJhaW4gJT4lDQogIG11dGF0ZShzZWcuZml0LkYgPSBicm9rZW4ubGluZShsbV9tb2RlbC5GKSRmaXQpDQoNCmdncGxvdChkZi5tb2RlbC5GLCBhZXMoeCA9IEJ1cm5hYnkxLCB5ID0gRkVNKSkgKyANCiAgZ2VvbV9wb2ludChhbHBoYT0wLjUpICsNCiAgZ2VvbV9saW5lKGFlcyh4ID0gQnVybmFieTEsIHkgPSBzZWcuZml0LkYpLCBjb2xvciA9ICdyZWQnLCBsaW5ldHlwZT0nZGFzaGVkJyxzaXplPTEpICsNCiAgeGxhYihleHByZXNzaW9uKFJlcG9ydGVkflNlbnNvcn5QTVsyLjVdfkNvbmNlbnRyYXRpb25+KM68Zy9tXjMpKSkgKw0KICB5bGFiKGV4cHJlc3Npb24oTWVhc3VyZWR+UE1bMi41XX5Db25jZW50cmF0aW9ufijOvGcvbV4zKSkpICsNCiAgZ2d0aXRsZSgiQnJlYWsgcG9pbnQgdmlzdWFsaXphdGlvbjogU2Vuc29yIEJ1cm5hYnkxIikgKw0KICB0aGVtZShwbG90LnRpdGxlID0gZWxlbWVudF90ZXh0KGhqdXN0ID0gMC41KSwgdGV4dCA9IGVsZW1lbnRfdGV4dChzaXplPTEyKSkNCmBgYA0KDQpDb25ncmF0dWxhdGlvbnMhIFlvdSBoYXZlIHJlYWNoZWQgdGhlIGVuZCBvZiB0aGUgdHV0b3JpYWwuIEkgaG9wZSBpdCB3YXMgaW5mb3JtYXRpdmUgYW5kIGZlZWwgZnJlZSB0byBjb250YWN0IG1lIChzZWUgdGhlICJBYm91dCIgcGFnZSkgaWYgeW91IGhhdmUgYW55IHF1ZXN0aW9ucy4NCg0KIyMgUmVmZXJlbmNlcw0K