1 - Description


In this project we will be building a recommender system using Singular Value Decomposition for beers. We will be using the data set from Beer Advocate which can be found on Data Wrold with a login required. We will also use linear regression to see if a set of summary statistics that we generate can be used to improve the SVD recommendation

2 - DataSet


We will start by loading in the data an taking a look at what is provided by Beer Advocate. We can see that along with information about the review and the beer, there is also information about the brewery and when the beer was reviewed. For this project we will drop these columns. The beer advocate data also contains 1.58 million reviews of various beers. For this project we want to reduce this number by limiting our system to only active reviewers and beers. To do this, We will remove all beers that have been reviewed less than 100 times and all reviewers that have reviewed less than 50 beers. This reduces our number of reviews to just under a million and should help to reduce the effects of reviewers and beers that are not active on the process of building a recommender system.

ratings <- read.csv("~/GitHub/DATA643/data/beeradvocate/beer_reviews.csv", header = TRUE, sep =",",
                    stringsAsFactors = FALSE)
head(ratings)
beer <- ratings %>%
  group_by(beer_beerid) %>%
  filter(n()>100) %>%
  group_by(review_profilename) %>%
  filter(n()>50) %>%
  select(-brewery_id, -brewery_name, -review_time)

3 Pre-Processing the Data


Now that we have our data set we want to do some pre-processing and summarizing work that will later be used to build out the SVD beer recommender. We start with creating the user-item matrix and then move on to constructing the summary statistics that we will use later.

3.1 - Building the User-Item Matrix

Given the type of data that we have here, there is the potential for repeated rows where a reviewer has reviewed a beer more than once. In fact, when we first attempted to used the spread() function of the tidyr package we got a duplicate identifiers error indicating that this was the case. Therefore we will use the dcast() function from reshape2 which includes the ability to aggregate the data when reshaping it to a user-item matrix as recommended in Professor Stern’s video. We can examine the top left corner and see that we have our expected user item matrix with some reviews visible.

# Generating the user-item matrix
user_beer <- dcast(beer, review_profilename~beer_name, 
                   value.var = "review_overall", fill=0, fun.aggregate = mean)
# Filling in rownames
rownames(user_beer) = user_beer$review_profilename
# Removing the first column
user_beer <- user_beer[,-1]
# Converting to a matrix
user_beer <- as.matrix(user_beer)
# Looking at the upper left corner
user_beer[1:6, 1:6]
           "Shabadoo" Black & Tan Ale # 100 #9 10 Commandments 10 Squared Fish Tale Ale 10.0
                                    0     0  0             0.0                        0    0
0110x011                            0     0  0             3.5                        0    0
05Harley                            0     0  0             0.0                        0    0
100floods                           0     5  0             0.0                        4    0
11osixBrew                          0     0  0             0.0                        0    0
1759Girl                            0     0  0             0.0                        0    0

3.2 - Generating the Summary Statiistics

We have decided to summarize some of the data columns that we have from the original data set to see if they may be of use in determining the final recommendation. We will also use this information to try to tie in what we know about linear regression to determine which of these measures are the most predictive for recommending to users. We will generate the following factors per user:

  1. Average of the users reviews (avg_reviews)
  2. Average of the users aroma reviews (avg_aroma)
  3. Average of the users appearance reviews (avg_appearance)
  4. Average of the users palate reviews (avg_palate)
  5. Average of the users taste reviews (avg_taste)
  6. The number of beers that each user reviewed, this will be used to calculate a more accurate average predicted review later in the process (beers_reviewed)
  7. The bias in a users reviews from the average review (review_bias)
  8. The bias in a users aroma reviews from the average review (aroma_bias)
  9. The bias in a users appearance reviews and the average review (appearance_bias)
  10. The bias in a users palate reviews from the average review (palate_bias)
  11. The bias in a users taste reviews from the average review (taste_bias)

We leverage some of the capabilities of the dplyr package to simplify this process.

beer_summary <- beer %>%
  group_by(review_profilename) %>%
  summarise(
    avg_review = mean(review_overall),
    avg_aroma = mean(review_aroma),
    avg_apperance = mean(review_appearance),
    avg_palate = mean(review_palate),
    avg_taste = mean(review_taste),
    beers_reviewed = n()
  ) %>%
  mutate(review_bias = avg_review - mean(avg_review),
         aroma_bias = avg_aroma - mean(avg_aroma),
         apperance_bias = avg_apperance - mean(avg_apperance),
         palate_bias = avg_palate - mean(avg_palate),
         taste_bias = avg_taste - mean(avg_taste)
  )

4 - Calculating the Singular Value Decomposition

There are multiple methods for doing singular value decomposition in R including the base svd() function. However this function tends to be inefficient and it is oft recommended that we use the irlba package. irlba is short for Implicitly Restarted Lanczos Bidiagonalization Algorithm which preforms a truncated singular value decomposition that is often more accurate then the original SVD algorithm. Therefore, we will use the irlba package to calculate the SVD for our user-item matrix.

# Computing the SVD
decomp = irlba(user_beer, nu = 3, nv = 3)
# Generating the prediction matrix
beerPredict = beer_summary$avg_review + (decomp$u * sqrt(decomp$d)) %*% (sqrt(decomp$d) * t(decomp$v))
# Renaming the rows and columns for easier lookups
colnames(beerPredict) <- colnames(user_beer)
rownames(beerPredict) <- rownames(user_beer)

One common method for evaluating a recommender system is by calculating the Root Mean Squared Error. This function takes the square root of the average difference squared between the predicted value and actual value at each point. We use the following function to calculate this value. One word of caution is that we need to have our matrix of actual values with NA’s in the missing spots, if we don’t then we run into the issue of our means being overwhelmed by the zeros in the matrix.

RMSE <- function(predictionMatrix, actualMatrix){
  sqrt(mean((predictionMatrix - actualMatrix)^2, na.rm=T))
}
user_beerNA <- user_beer
is.na(user_beerNA) <- user_beerNA == 0
RMSE(beerPredict, user_beerNA)
[1] 1.379416

We actually find from looking at the RMSE score that the SVD model has done a solid job of by being, on average, 1.4 stars away from the actual rating.

5 - Building the Recommender


In this step we will do two things, we first build a function to handle doing basic recommendations based on the one shared by Professor Stern. We then attempt to fit a linear regression to the variables that we calculated above to see if we can get a better recommender based on our other statistics.

5.1 Using the SVD to build a Recommender

Now that we have our matrix of predicted reviews the recommender will do one of the following. If the user has reviewed the beer before it will give them back their previous review. If they have not reviewed the beer before then it returns the predicted value generated from the SVD matrix.

getBeer <- function(user, beer){
  if(user_beer[user,beer] != 0){
    paste("Previously Rated:", user_beer[user,beer])
  }
  else{
    paste("Predicted Rating:", round(beerPredict[user,beer],1))
  }
}

Now that we have our recommender we will put it through it’s paces to see if it is generating predictions that make sense. We see that the recommender is working as expected when we present a user with a beer that they have not rated we get a value that seems to be in a reasonable range, we also see that when we present a user with a beer that has already been rated we are returned with the users previous rating.

getBeer("BeerLover99", "Alaskan Smoked Porter")
[1] "Predicted Rating: 4.3"
getBeer("2xHops", "#9")
[1] "Previously Rated: 3"

5.2 Can we improve the ratings with our Summay Information

In this last section we will see if we can update our model by weighting the summary statistic for each user. We have decided to try and use the summary variables to predict the users actual average number of beers weighted. We see that on average our recommender seems to be over rating the beers. We first create and average predicted review column in our data. We also take a look at the the average of the reviewed values and note that the predicted values are slightly higher on average then the actual values.

beer_summary$predicted <- apply(beerPredict, 1, mean, na.rm = T)
paste("Actual Average:", mean(beer_summary$avg_review), " Predicted Average:", mean(beer_summary$predicted))
[1] "Actual Average: 3.8708155194761  Predicted Average: 4.05970054333809"

We next fit a linear model to the data to see which of our summary statistics have a significant impact on the predicted value.

fit <- lm(beer_summary$avg_review ~ . -review_profilename, data=beer_summary)
summary(fit)

Call:
lm(formula = beer_summary$avg_review ~ . - review_profilename, 
    data = beer_summary)

Residuals:
       Min         1Q     Median         3Q        Max 
-1.149e-14 -1.050e-16 -6.800e-17 -3.700e-17  2.898e-13 

Coefficients: (4 not defined because of singularities)
                 Estimate Std. Error    t value Pr(>|t|)    
(Intercept)     3.871e+00  3.809e-15  1.016e+15  < 2e-16 ***
avg_aroma       2.503e-15  6.624e-16  3.779e+00 0.000159 ***
avg_apperance  -1.144e-15  6.378e-16 -1.794e+00 0.072830 .  
avg_palate      1.989e-15  7.805e-16  2.548e+00 0.010865 *  
avg_taste      -1.671e-15  7.563e-16 -2.210e+00 0.027181 *  
beers_reviewed -4.108e-19  6.773e-19 -6.070e-01 0.544197    
review_bias     1.000e+00  1.015e-15  9.853e+14  < 2e-16 ***
aroma_bias             NA         NA         NA       NA    
apperance_bias         NA         NA         NA       NA    
palate_bias            NA         NA         NA       NA    
taste_bias             NA         NA         NA       NA    
predicted       4.504e-16  6.902e-16  6.530e-01 0.514069    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 4.511e-15 on 4168 degrees of freedom
Multiple R-squared:      1, Adjusted R-squared:      1 
F-statistic: 1.29e+30 on 7 and 4168 DF,  p-value: < 2.2e-16

It appears that we should add in the reviewers bias into the estimates to get a better predictor of the actual review. While other factors have a significant impact on the regression we see from their estimated coefficients that the impact is very minimal. Lets see what happens when we add that to our predictor.

fit <- lm(beer_summary$avg_review ~ beer_summary$predicted + 
            beer_summary$review_bias, data=beer_summary)
summary(fit)

Call:
lm(formula = beer_summary$avg_review ~ beer_summary$predicted + 
    beer_summary$review_bias, data = beer_summary)

Residuals:
       Min         1Q     Median         3Q        Max 
-3.250e-16 -8.800e-17 -7.500e-17 -6.500e-17  2.895e-13 

Coefficients:
                           Estimate Std. Error    t value Pr(>|t|)    
(Intercept)               3.871e+00  1.190e-15  3.253e+15  < 2e-16 ***
beer_summary$predicted   -1.286e-15  2.926e-16 -4.397e+00 1.13e-05 ***
beer_summary$review_bias  1.000e+00  4.470e-16  2.237e+15  < 2e-16 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 4.512e-15 on 4173 degrees of freedom
Multiple R-squared:      1, Adjusted R-squared:      1 
F-statistic: 4.513e+30 on 2 and 4173 DF,  p-value: < 2.2e-16

It appears that we should add in the reviewers bias into the estimates to get a better predictor of the actual review. Lets see what happens when we add that to our predictor.

getBeerRating <- function(user, beer){
  if(user_beer[user,beer] != 0){
    paste("Previously Rated:", user_beer[user,beer])
  }
  else{
    predicted = round(beerPredict[user,beer] + 
                        beer_summary$review_bias[beer_summary$review_profilename == user], 1)
    paste("Predicted Rating:", predicted)
  }
}

Looking at the same user beer combination as before, we see that the predicted rating is lowered slightly from the base prediction.

getBeerRating("BeerLover99", "Alaskan Smoked Porter")
[1] "Predicted Rating: 4.2"

Finally lets take a look at what happens to the RMSE when we take all of the predicted values and add the user bias to the predicted values. Interestingly enough we see that the RMSE is actually increased adding in the user bias to the predictions.

beerPredict2 <- sweep(beerPredict, 1, beer_summary$review_bias, "+")
RMSE(beerPredict2, user_beerNA)
[1] 1.389797

6 - Conclusion

This project really helped us to get a stronger grasp on the Singular Value Decomposition process. It was interesting to see how this process works when compared to constructing the collaborative filtering models using the recommmenderlab package. From what we have done here it will be interesting to see the techniques for using information related to the ratings to help improve the predictive power of our recommender systems. we were a bit surprised to see that the information gathered from the linear regression process actually made the recommendation a bit worse. We would also liked to have been able to explore the SVD potion of the recomenderlab package but unfortunately ran out of time to explore this.

LS0tDQp0aXRsZTogJ0RBVEE2NDMgLSBQcm9qZWN0IDM6IFJlY29tbWVuZGVyIFN5c3RlbSB1c2luZyBTaW5ndWxhciBWYWx1ZSBEZWNvbXBvc2l0aW9uJw0KYXV0aG9yOiAiRXJpayBOeWxhbmRlciINCm91dHB1dDoNCiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdA0KICBodG1sX2RvY3VtZW50OiBkZWZhdWx0DQogIHBkZl9kb2N1bWVudDogZGVmYXVsdA0KLS0tDQoNCmBgYHtSLCBpbmNsdWRlID0gRkFMU0V9DQpsaWJyYXJ5KGRwbHlyKQ0KbGlicmFyeSh0aWR5cikNCmxpYnJhcnkoZ2dwbG90MikNCmxpYnJhcnkocmVjb21tZW5kZXJsYWIpDQpsaWJyYXJ5KGlybGJhKQ0KbGlicmFyeShyZXNoYXBlMikNCmBgYA0KDQojIyAxIC0gRGVzY3JpcHRpb24NCioqKg0KSW4gdGhpcyBwcm9qZWN0IHdlIHdpbGwgYmUgYnVpbGRpbmcgYSByZWNvbW1lbmRlciBzeXN0ZW0gdXNpbmcgU2luZ3VsYXIgVmFsdWUgRGVjb21wb3NpdGlvbiBmb3IgYmVlcnMuIFdlIHdpbGwgYmUgdXNpbmcgdGhlIGRhdGEgc2V0IGZyb20gW0JlZXIgQWR2b2NhdGVdKGh0dHBzOi8vd3d3LmJlZXJhZHZvY2F0ZS5jb20vKSB3aGljaCBjYW4gYmUgZm91bmQgb24gW0RhdGEgV3JvbGRdKGh0dHBzOi8vZGF0YS53b3JsZC9zb2NpYWxtZWRpYWRhdGEvYmVlcmFkdm9jYXRlKSB3aXRoIGEgbG9naW4gcmVxdWlyZWQuIFdlIHdpbGwgYWxzbyB1c2UgbGluZWFyIHJlZ3Jlc3Npb24gdG8gc2VlIGlmIGEgc2V0IG9mIHN1bW1hcnkgc3RhdGlzdGljcyB0aGF0IHdlIGdlbmVyYXRlIGNhbiBiZSB1c2VkIHRvIGltcHJvdmUgdGhlIFNWRCByZWNvbW1lbmRhdGlvbg0KDQojIyAyIC0gRGF0YVNldA0KKioqDQpXZSB3aWxsIHN0YXJ0IGJ5IGxvYWRpbmcgaW4gdGhlIGRhdGEgYW4gdGFraW5nIGEgbG9vayBhdCB3aGF0IGlzIHByb3ZpZGVkIGJ5IEJlZXIgQWR2b2NhdGUuIFdlIGNhbiBzZWUgdGhhdCBhbG9uZyB3aXRoIGluZm9ybWF0aW9uIGFib3V0IHRoZSByZXZpZXcgYW5kIHRoZSBiZWVyLCB0aGVyZSBpcyBhbHNvIGluZm9ybWF0aW9uIGFib3V0IHRoZSBicmV3ZXJ5IGFuZCB3aGVuIHRoZSBiZWVyIHdhcyByZXZpZXdlZC4gRm9yIHRoaXMgcHJvamVjdCB3ZSB3aWxsIGRyb3AgdGhlc2UgY29sdW1ucy4gVGhlIGJlZXIgYWR2b2NhdGUgZGF0YSBhbHNvIGNvbnRhaW5zIDEuNTggbWlsbGlvbiByZXZpZXdzIG9mIHZhcmlvdXMgYmVlcnMuIEZvciB0aGlzIHByb2plY3Qgd2Ugd2FudCB0byByZWR1Y2UgdGhpcyBudW1iZXIgYnkgbGltaXRpbmcgb3VyIHN5c3RlbSB0byBvbmx5IGFjdGl2ZSByZXZpZXdlcnMgYW5kIGJlZXJzLiBUbyBkbyB0aGlzLCBXZSB3aWxsIHJlbW92ZSBhbGwgYmVlcnMgdGhhdCBoYXZlIGJlZW4gcmV2aWV3ZWQgbGVzcyB0aGFuIDEwMCB0aW1lcyBhbmQgYWxsIHJldmlld2VycyB0aGF0IGhhdmUgcmV2aWV3ZWQgbGVzcyB0aGFuIDUwIGJlZXJzLiBUaGlzIHJlZHVjZXMgb3VyIG51bWJlciBvZiByZXZpZXdzIHRvIGp1c3QgdW5kZXIgYSBtaWxsaW9uIGFuZCBzaG91bGQgaGVscCB0byByZWR1Y2UgdGhlIGVmZmVjdHMgb2YgcmV2aWV3ZXJzIGFuZCBiZWVycyB0aGF0IGFyZSBub3QgYWN0aXZlIG9uIHRoZSBwcm9jZXNzIG9mIGJ1aWxkaW5nIGEgcmVjb21tZW5kZXIgc3lzdGVtLiANCmBgYHtSLCBpbmNsdWRlID0gVFJVRX0NCnJhdGluZ3MgPC0gcmVhZC5jc3YoIn4vR2l0SHViL0RBVEE2NDMvZGF0YS9iZWVyYWR2b2NhdGUvYmVlcl9yZXZpZXdzLmNzdiIsIGhlYWRlciA9IFRSVUUsIHNlcCA9IiwiLA0KICAgICAgICAgICAgICAgICAgICBzdHJpbmdzQXNGYWN0b3JzID0gRkFMU0UpDQoNCmhlYWQocmF0aW5ncykNCmBgYA0KDQpgYGB7Un0NCmJlZXIgPC0gcmF0aW5ncyAlPiUNCiAgZ3JvdXBfYnkoYmVlcl9iZWVyaWQpICU+JQ0KICBmaWx0ZXIobigpPjEwMCkgJT4lDQogIGdyb3VwX2J5KHJldmlld19wcm9maWxlbmFtZSkgJT4lDQogIGZpbHRlcihuKCk+NTApICU+JQ0KICBzZWxlY3QoLWJyZXdlcnlfaWQsIC1icmV3ZXJ5X25hbWUsIC1yZXZpZXdfdGltZSkNCmBgYA0KDQojIyAzIFByZS1Qcm9jZXNzaW5nIHRoZSBEYXRhDQoqKioNCk5vdyB0aGF0IHdlIGhhdmUgb3VyIGRhdGEgc2V0IHdlIHdhbnQgdG8gZG8gc29tZSBwcmUtcHJvY2Vzc2luZyBhbmQgc3VtbWFyaXppbmcgd29yayB0aGF0IHdpbGwgbGF0ZXIgYmUgdXNlZCB0byBidWlsZCBvdXQgdGhlIFNWRCBiZWVyIHJlY29tbWVuZGVyLiBXZSBzdGFydCB3aXRoIGNyZWF0aW5nIHRoZSB1c2VyLWl0ZW0gbWF0cml4IGFuZCB0aGVuIG1vdmUgb24gdG8gY29uc3RydWN0aW5nIHRoZSBzdW1tYXJ5IHN0YXRpc3RpY3MgdGhhdCB3ZSB3aWxsIHVzZSBsYXRlci4NCg0KIyMjIyAzLjEgLSBCdWlsZGluZyB0aGUgVXNlci1JdGVtIE1hdHJpeA0KR2l2ZW4gdGhlIHR5cGUgb2YgZGF0YSB0aGF0IHdlIGhhdmUgaGVyZSwgdGhlcmUgaXMgdGhlIHBvdGVudGlhbCBmb3IgcmVwZWF0ZWQgcm93cyB3aGVyZSBhIHJldmlld2VyIGhhcyByZXZpZXdlZCBhIGJlZXIgbW9yZSB0aGFuIG9uY2UuIEluIGZhY3QsIHdoZW4gd2UgZmlyc3QgYXR0ZW1wdGVkIHRvIHVzZWQgdGhlIHNwcmVhZCgpIGZ1bmN0aW9uIG9mIHRoZSAqdGlkeXIqIHBhY2thZ2Ugd2UgZ290IGEgZHVwbGljYXRlIGlkZW50aWZpZXJzIGVycm9yIGluZGljYXRpbmcgdGhhdCB0aGlzIHdhcyB0aGUgY2FzZS4gVGhlcmVmb3JlIHdlIHdpbGwgdXNlIHRoZSBkY2FzdCgpIGZ1bmN0aW9uIGZyb20gKnJlc2hhcGUyKiB3aGljaCBpbmNsdWRlcyB0aGUgYWJpbGl0eSB0byBhZ2dyZWdhdGUgdGhlIGRhdGEgd2hlbiByZXNoYXBpbmcgaXQgdG8gYSB1c2VyLWl0ZW0gbWF0cml4IGFzIHJlY29tbWVuZGVkIGluIFByb2Zlc3NvciBTdGVybidzIHZpZGVvLiBXZSBjYW4gZXhhbWluZSB0aGUgdG9wIGxlZnQgY29ybmVyIGFuZCBzZWUgdGhhdCB3ZSBoYXZlIG91ciBleHBlY3RlZCB1c2VyIGl0ZW0gbWF0cml4IHdpdGggc29tZSByZXZpZXdzIHZpc2libGUuDQoNCmBgYHtSfQ0KIyBHZW5lcmF0aW5nIHRoZSB1c2VyLWl0ZW0gbWF0cml4DQp1c2VyX2JlZXIgPC0gZGNhc3QoYmVlciwgcmV2aWV3X3Byb2ZpbGVuYW1lfmJlZXJfbmFtZSwgDQogICAgICAgICAgICAgICAgICAgdmFsdWUudmFyID0gInJldmlld19vdmVyYWxsIiwgZmlsbD0wLCBmdW4uYWdncmVnYXRlID0gbWVhbikNCg0KIyBGaWxsaW5nIGluIHJvd25hbWVzDQpyb3duYW1lcyh1c2VyX2JlZXIpID0gdXNlcl9iZWVyJHJldmlld19wcm9maWxlbmFtZQ0KDQojIFJlbW92aW5nIHRoZSBmaXJzdCBjb2x1bW4NCnVzZXJfYmVlciA8LSB1c2VyX2JlZXJbLC0xXQ0KDQojIENvbnZlcnRpbmcgdG8gYSBtYXRyaXgNCnVzZXJfYmVlciA8LSBhcy5tYXRyaXgodXNlcl9iZWVyKQ0KDQojIExvb2tpbmcgYXQgdGhlIHVwcGVyIGxlZnQgY29ybmVyDQp1c2VyX2JlZXJbMTo2LCAxOjZdDQpgYGANCg0KIyMjIyAzLjIgLSBHZW5lcmF0aW5nIHRoZSBTdW1tYXJ5IFN0YXRpaXN0aWNzDQpXZSBoYXZlIGRlY2lkZWQgdG8gc3VtbWFyaXplIHNvbWUgb2YgdGhlIGRhdGEgY29sdW1ucyB0aGF0IHdlIGhhdmUgZnJvbSB0aGUgb3JpZ2luYWwgZGF0YSBzZXQgdG8gc2VlIGlmIHRoZXkgbWF5IGJlIG9mIHVzZSBpbiBkZXRlcm1pbmluZyB0aGUgZmluYWwgcmVjb21tZW5kYXRpb24uIFdlIHdpbGwgYWxzbyB1c2UgdGhpcyBpbmZvcm1hdGlvbiB0byB0cnkgdG8gdGllIGluIHdoYXQgd2Uga25vdyBhYm91dCBsaW5lYXIgcmVncmVzc2lvbiB0byBkZXRlcm1pbmUgd2hpY2ggb2YgdGhlc2UgbWVhc3VyZXMgYXJlIHRoZSBtb3N0IHByZWRpY3RpdmUgZm9yIHJlY29tbWVuZGluZyB0byB1c2Vycy4gV2Ugd2lsbCBnZW5lcmF0ZSB0aGUgZm9sbG93aW5nIGZhY3RvcnMgcGVyIHVzZXI6ICANCg0KMS4gQXZlcmFnZSBvZiB0aGUgdXNlcnMgcmV2aWV3cyAoYXZnX3Jldmlld3MpICANCjIuIEF2ZXJhZ2Ugb2YgdGhlIHVzZXJzIGFyb21hIHJldmlld3MgKGF2Z19hcm9tYSkgIA0KMy4gQXZlcmFnZSBvZiB0aGUgdXNlcnMgYXBwZWFyYW5jZSByZXZpZXdzIChhdmdfYXBwZWFyYW5jZSkNCjQuIEF2ZXJhZ2Ugb2YgdGhlIHVzZXJzIHBhbGF0ZSByZXZpZXdzIChhdmdfcGFsYXRlKQ0KNS4gQXZlcmFnZSBvZiB0aGUgdXNlcnMgdGFzdGUgcmV2aWV3cyAoYXZnX3Rhc3RlKQ0KNi4gVGhlIG51bWJlciBvZiBiZWVycyB0aGF0IGVhY2ggdXNlciByZXZpZXdlZCwgdGhpcyB3aWxsIGJlIHVzZWQgdG8gY2FsY3VsYXRlIGEgbW9yZSBhY2N1cmF0ZSBhdmVyYWdlIHByZWRpY3RlZCByZXZpZXcgbGF0ZXIgaW4gdGhlIHByb2Nlc3MgKGJlZXJzX3Jldmlld2VkKQ0KNy4gVGhlIGJpYXMgaW4gYSB1c2VycyByZXZpZXdzIGZyb20gdGhlIGF2ZXJhZ2UgcmV2aWV3IChyZXZpZXdfYmlhcykNCjguIFRoZSBiaWFzIGluIGEgdXNlcnMgYXJvbWEgcmV2aWV3cyBmcm9tIHRoZSBhdmVyYWdlIHJldmlldyAoYXJvbWFfYmlhcykNCjkuIFRoZSBiaWFzIGluIGEgdXNlcnMgYXBwZWFyYW5jZSByZXZpZXdzIGFuZCB0aGUgYXZlcmFnZSByZXZpZXcgKGFwcGVhcmFuY2VfYmlhcykNCjEwLiBUaGUgYmlhcyBpbiBhIHVzZXJzIHBhbGF0ZSByZXZpZXdzIGZyb20gdGhlIGF2ZXJhZ2UgcmV2aWV3IChwYWxhdGVfYmlhcykNCjExLiBUaGUgYmlhcyBpbiBhIHVzZXJzIHRhc3RlIHJldmlld3MgZnJvbSB0aGUgYXZlcmFnZSByZXZpZXcgKHRhc3RlX2JpYXMpDQoNCldlIGxldmVyYWdlIHNvbWUgb2YgdGhlIGNhcGFiaWxpdGllcyBvZiB0aGUgKmRwbHlyKiBwYWNrYWdlIHRvIHNpbXBsaWZ5IHRoaXMgcHJvY2Vzcy4gDQoNCmBgYHtSfQ0KYmVlcl9zdW1tYXJ5IDwtIGJlZXIgJT4lDQogIGdyb3VwX2J5KHJldmlld19wcm9maWxlbmFtZSkgJT4lDQogIHN1bW1hcmlzZSgNCiAgICBhdmdfcmV2aWV3ID0gbWVhbihyZXZpZXdfb3ZlcmFsbCksDQogICAgYXZnX2Fyb21hID0gbWVhbihyZXZpZXdfYXJvbWEpLA0KICAgIGF2Z19hcHBlcmFuY2UgPSBtZWFuKHJldmlld19hcHBlYXJhbmNlKSwNCiAgICBhdmdfcGFsYXRlID0gbWVhbihyZXZpZXdfcGFsYXRlKSwNCiAgICBhdmdfdGFzdGUgPSBtZWFuKHJldmlld190YXN0ZSksDQogICAgYmVlcnNfcmV2aWV3ZWQgPSBuKCkNCiAgKSAlPiUNCiAgbXV0YXRlKHJldmlld19iaWFzID0gYXZnX3JldmlldyAtIG1lYW4oYXZnX3JldmlldyksDQogICAgICAgICBhcm9tYV9iaWFzID0gYXZnX2Fyb21hIC0gbWVhbihhdmdfYXJvbWEpLA0KICAgICAgICAgYXBwZXJhbmNlX2JpYXMgPSBhdmdfYXBwZXJhbmNlIC0gbWVhbihhdmdfYXBwZXJhbmNlKSwNCiAgICAgICAgIHBhbGF0ZV9iaWFzID0gYXZnX3BhbGF0ZSAtIG1lYW4oYXZnX3BhbGF0ZSksDQogICAgICAgICB0YXN0ZV9iaWFzID0gYXZnX3Rhc3RlIC0gbWVhbihhdmdfdGFzdGUpDQogICkNCmBgYA0KDQojIyA0IC0gQ2FsY3VsYXRpbmcgdGhlIFNpbmd1bGFyIFZhbHVlIERlY29tcG9zaXRpb24NClRoZXJlIGFyZSBtdWx0aXBsZSBtZXRob2RzIGZvciBkb2luZyBzaW5ndWxhciB2YWx1ZSBkZWNvbXBvc2l0aW9uIGluIFIgaW5jbHVkaW5nIHRoZSBiYXNlIHN2ZCgpIGZ1bmN0aW9uLiBIb3dldmVyIHRoaXMgZnVuY3Rpb24gdGVuZHMgdG8gYmUgaW5lZmZpY2llbnQgYW5kIGl0IGlzIG9mdCByZWNvbW1lbmRlZCB0aGF0IHdlIHVzZSB0aGUgKmlybGJhKiBwYWNrYWdlLiAqaXJsYmEqIGlzIHNob3J0IGZvciBJbXBsaWNpdGx5IFJlc3RhcnRlZCBMYW5jem9zIEJpZGlhZ29uYWxpemF0aW9uIEFsZ29yaXRobSB3aGljaCBwcmVmb3JtcyBhIHRydW5jYXRlZCBzaW5ndWxhciB2YWx1ZSBkZWNvbXBvc2l0aW9uIHRoYXQgaXMgb2Z0ZW4gbW9yZSBhY2N1cmF0ZSB0aGVuIHRoZSBvcmlnaW5hbCBTVkQgYWxnb3JpdGhtLiBUaGVyZWZvcmUsIHdlIHdpbGwgdXNlIHRoZSAqaXJsYmEqIHBhY2thZ2UgdG8gY2FsY3VsYXRlIHRoZSBTVkQgZm9yIG91ciB1c2VyLWl0ZW0gbWF0cml4Lg0KYGBge1J9DQojIENvbXB1dGluZyB0aGUgU1ZEDQpkZWNvbXAgPSBpcmxiYSh1c2VyX2JlZXIsIG51ID0gMywgbnYgPSAzKQ0KDQojIEdlbmVyYXRpbmcgdGhlIHByZWRpY3Rpb24gbWF0cml4DQpiZWVyUHJlZGljdCA9IGJlZXJfc3VtbWFyeSRhdmdfcmV2aWV3ICsgKGRlY29tcCR1ICogc3FydChkZWNvbXAkZCkpICUqJSAoc3FydChkZWNvbXAkZCkgKiB0KGRlY29tcCR2KSkNCg0KIyBSZW5hbWluZyB0aGUgcm93cyBhbmQgY29sdW1ucyBmb3IgZWFzaWVyIGxvb2t1cHMNCmNvbG5hbWVzKGJlZXJQcmVkaWN0KSA8LSBjb2xuYW1lcyh1c2VyX2JlZXIpDQpyb3duYW1lcyhiZWVyUHJlZGljdCkgPC0gcm93bmFtZXModXNlcl9iZWVyKQ0KYGBgDQoNCk9uZSBjb21tb24gbWV0aG9kIGZvciBldmFsdWF0aW5nIGEgcmVjb21tZW5kZXIgc3lzdGVtIGlzIGJ5IGNhbGN1bGF0aW5nIHRoZSBSb290IE1lYW4gU3F1YXJlZCBFcnJvci4gVGhpcyBmdW5jdGlvbiB0YWtlcyB0aGUgc3F1YXJlIHJvb3Qgb2YgdGhlIGF2ZXJhZ2UgZGlmZmVyZW5jZSBzcXVhcmVkIGJldHdlZW4gdGhlIHByZWRpY3RlZCB2YWx1ZSBhbmQgYWN0dWFsIHZhbHVlIGF0IGVhY2ggcG9pbnQuIFdlIHVzZSB0aGUgZm9sbG93aW5nIGZ1bmN0aW9uIHRvIGNhbGN1bGF0ZSB0aGlzIHZhbHVlLiBPbmUgd29yZCBvZiBjYXV0aW9uIGlzIHRoYXQgd2UgbmVlZCB0byBoYXZlIG91ciBtYXRyaXggb2YgYWN0dWFsIHZhbHVlcyB3aXRoIE5BJ3MgaW4gdGhlIG1pc3Npbmcgc3BvdHMsIGlmIHdlIGRvbid0IHRoZW4gd2UgcnVuIGludG8gdGhlIGlzc3VlIG9mIG91ciBtZWFucyBiZWluZyBvdmVyd2hlbG1lZCBieSB0aGUgemVyb3MgaW4gdGhlIG1hdHJpeC4gDQpgYGB7Un0NClJNU0UgPC0gZnVuY3Rpb24ocHJlZGljdGlvbk1hdHJpeCwgYWN0dWFsTWF0cml4KXsNCiAgc3FydChtZWFuKChwcmVkaWN0aW9uTWF0cml4IC0gYWN0dWFsTWF0cml4KV4yLCBuYS5ybT1UKSkNCn0NCg0KdXNlcl9iZWVyTkEgPC0gdXNlcl9iZWVyDQppcy5uYSh1c2VyX2JlZXJOQSkgPC0gdXNlcl9iZWVyTkEgPT0gMA0KUk1TRShiZWVyUHJlZGljdCwgdXNlcl9iZWVyTkEpDQpgYGANCg0KV2UgYWN0dWFsbHkgZmluZCBmcm9tIGxvb2tpbmcgYXQgdGhlIFJNU0Ugc2NvcmUgdGhhdCB0aGUgU1ZEIG1vZGVsIGhhcyBkb25lIGEgc29saWQgam9iIG9mIGJ5IGJlaW5nLCBvbiBhdmVyYWdlLCAxLjQgc3RhcnMgYXdheSBmcm9tIHRoZSBhY3R1YWwgcmF0aW5nLg0KDQojIyA1IC0gQnVpbGRpbmcgdGhlIFJlY29tbWVuZGVyDQoqKioNCkluIHRoaXMgc3RlcCB3ZSB3aWxsIGRvIHR3byB0aGluZ3MsIHdlIGZpcnN0IGJ1aWxkIGEgZnVuY3Rpb24gdG8gaGFuZGxlIGRvaW5nIGJhc2ljIHJlY29tbWVuZGF0aW9ucyBiYXNlZCBvbiB0aGUgb25lIHNoYXJlZCBieSBQcm9mZXNzb3IgU3Rlcm4uIFdlIHRoZW4gYXR0ZW1wdCB0byBmaXQgYSBsaW5lYXIgcmVncmVzc2lvbiB0byB0aGUgdmFyaWFibGVzIHRoYXQgd2UgY2FsY3VsYXRlZCBhYm92ZSB0byBzZWUgaWYgd2UgY2FuIGdldCBhIGJldHRlciByZWNvbW1lbmRlciBiYXNlZCBvbiBvdXIgb3RoZXIgc3RhdGlzdGljcy4NCg0KIyMjIyA1LjEgVXNpbmcgdGhlIFNWRCB0byBidWlsZCBhIFJlY29tbWVuZGVyDQpOb3cgdGhhdCB3ZSBoYXZlIG91ciBtYXRyaXggb2YgcHJlZGljdGVkIHJldmlld3MgdGhlIHJlY29tbWVuZGVyIHdpbGwgZG8gb25lIG9mIHRoZSBmb2xsb3dpbmcuIElmIHRoZSB1c2VyIGhhcyByZXZpZXdlZCB0aGUgYmVlciBiZWZvcmUgaXQgd2lsbCBnaXZlIHRoZW0gYmFjayB0aGVpciBwcmV2aW91cyByZXZpZXcuIElmIHRoZXkgaGF2ZSBub3QgcmV2aWV3ZWQgdGhlIGJlZXIgYmVmb3JlIHRoZW4gaXQgcmV0dXJucyB0aGUgcHJlZGljdGVkIHZhbHVlIGdlbmVyYXRlZCBmcm9tIHRoZSBTVkQgbWF0cml4Lg0KYGBge1J9DQpnZXRCZWVyIDwtIGZ1bmN0aW9uKHVzZXIsIGJlZXIpew0KICBpZih1c2VyX2JlZXJbdXNlcixiZWVyXSAhPSAwKXsNCiAgICBwYXN0ZSgiUHJldmlvdXNseSBSYXRlZDoiLCB1c2VyX2JlZXJbdXNlcixiZWVyXSkNCiAgfQ0KICBlbHNlew0KICAgIHBhc3RlKCJQcmVkaWN0ZWQgUmF0aW5nOiIsIHJvdW5kKGJlZXJQcmVkaWN0W3VzZXIsYmVlcl0sMSkpDQogIH0NCn0NCmBgYA0KDQpOb3cgdGhhdCB3ZSBoYXZlIG91ciByZWNvbW1lbmRlciB3ZSB3aWxsIHB1dCBpdCB0aHJvdWdoIGl0J3MgcGFjZXMgdG8gc2VlIGlmIGl0IGlzIGdlbmVyYXRpbmcgcHJlZGljdGlvbnMgdGhhdCBtYWtlIHNlbnNlLiBXZSBzZWUgdGhhdCB0aGUgcmVjb21tZW5kZXIgaXMgd29ya2luZyBhcyBleHBlY3RlZCB3aGVuIHdlIHByZXNlbnQgYSB1c2VyIHdpdGggYSBiZWVyIHRoYXQgdGhleSBoYXZlIG5vdCByYXRlZCB3ZSBnZXQgYSB2YWx1ZSB0aGF0IHNlZW1zIHRvIGJlIGluIGEgcmVhc29uYWJsZSByYW5nZSwgd2UgYWxzbyBzZWUgdGhhdCB3aGVuIHdlIHByZXNlbnQgYSB1c2VyIHdpdGggYSBiZWVyIHRoYXQgaGFzIGFscmVhZHkgYmVlbiByYXRlZCB3ZSBhcmUgcmV0dXJuZWQgd2l0aCB0aGUgdXNlcnMgcHJldmlvdXMgcmF0aW5nLg0KYGBge1J9DQpnZXRCZWVyKCJCZWVyTG92ZXI5OSIsICJBbGFza2FuIFNtb2tlZCBQb3J0ZXIiKQ0KYGBgDQoNCmBgYHtSfQ0KZ2V0QmVlcigiMnhIb3BzIiwgIiM5IikNCmBgYA0KDQojIyMjIDUuMiBDYW4gd2UgaW1wcm92ZSB0aGUgcmF0aW5ncyB3aXRoIG91ciBTdW1tYXkgSW5mb3JtYXRpb24NCkluIHRoaXMgbGFzdCBzZWN0aW9uIHdlIHdpbGwgc2VlIGlmIHdlIGNhbiB1cGRhdGUgb3VyIG1vZGVsIGJ5IHdlaWdodGluZyB0aGUgc3VtbWFyeSBzdGF0aXN0aWMgZm9yIGVhY2ggdXNlci4gV2UgaGF2ZSBkZWNpZGVkIHRvIHRyeSBhbmQgdXNlIHRoZSBzdW1tYXJ5IHZhcmlhYmxlcyB0byBwcmVkaWN0IHRoZSB1c2VycyBhY3R1YWwgYXZlcmFnZSBudW1iZXIgb2YgYmVlcnMgd2VpZ2h0ZWQuIFdlIHNlZSB0aGF0IG9uIGF2ZXJhZ2Ugb3VyIHJlY29tbWVuZGVyIHNlZW1zIHRvIGJlIG92ZXIgcmF0aW5nIHRoZSBiZWVycy4gV2UgZmlyc3QgY3JlYXRlIGFuZCBhdmVyYWdlIHByZWRpY3RlZCByZXZpZXcgY29sdW1uIGluIG91ciBkYXRhLiBXZSBhbHNvIHRha2UgYSBsb29rIGF0IHRoZSB0aGUgYXZlcmFnZSBvZiB0aGUgcmV2aWV3ZWQgdmFsdWVzIGFuZCBub3RlIHRoYXQgdGhlIHByZWRpY3RlZCB2YWx1ZXMgYXJlIHNsaWdodGx5IGhpZ2hlciBvbiBhdmVyYWdlIHRoZW4gdGhlIGFjdHVhbCB2YWx1ZXMuDQpgYGB7Un0NCmJlZXJfc3VtbWFyeSRwcmVkaWN0ZWQgPC0gYXBwbHkoYmVlclByZWRpY3QsIDEsIG1lYW4sIG5hLnJtID0gVCkNCnBhc3RlKCJBY3R1YWwgQXZlcmFnZToiLCBtZWFuKGJlZXJfc3VtbWFyeSRhdmdfcmV2aWV3KSwgIiBQcmVkaWN0ZWQgQXZlcmFnZToiLCBtZWFuKGJlZXJfc3VtbWFyeSRwcmVkaWN0ZWQpKQ0KYGBgDQoNCldlIG5leHQgZml0IGEgbGluZWFyIG1vZGVsIHRvIHRoZSBkYXRhIHRvIHNlZSB3aGljaCBvZiBvdXIgc3VtbWFyeSBzdGF0aXN0aWNzIGhhdmUgYSBzaWduaWZpY2FudCBpbXBhY3Qgb24gdGhlIHByZWRpY3RlZCB2YWx1ZS4NCmBgYHtSfQ0KZml0IDwtIGxtKGJlZXJfc3VtbWFyeSRhdmdfcmV2aWV3IH4gLiAtcmV2aWV3X3Byb2ZpbGVuYW1lLCBkYXRhPWJlZXJfc3VtbWFyeSkNCnN1bW1hcnkoZml0KQ0KYGBgDQoNCkl0IGFwcGVhcnMgdGhhdCB3ZSBzaG91bGQgYWRkIGluIHRoZSByZXZpZXdlcnMgYmlhcyBpbnRvIHRoZSBlc3RpbWF0ZXMgdG8gZ2V0IGEgYmV0dGVyIHByZWRpY3RvciBvZiB0aGUgYWN0dWFsIHJldmlldy4gV2hpbGUgb3RoZXIgZmFjdG9ycyBoYXZlIGEgc2lnbmlmaWNhbnQgaW1wYWN0IG9uIHRoZSByZWdyZXNzaW9uIHdlIHNlZSBmcm9tIHRoZWlyIGVzdGltYXRlZCBjb2VmZmljaWVudHMgdGhhdCB0aGUgaW1wYWN0IGlzIHZlcnkgbWluaW1hbC4gTGV0cyBzZWUgd2hhdCBoYXBwZW5zIHdoZW4gd2UgYWRkIHRoYXQgdG8gb3VyIHByZWRpY3Rvci4NCg0KYGBge1J9DQpmaXQgPC0gbG0oYmVlcl9zdW1tYXJ5JGF2Z19yZXZpZXcgfiBiZWVyX3N1bW1hcnkkcHJlZGljdGVkICsgDQogICAgICAgICAgICBiZWVyX3N1bW1hcnkkcmV2aWV3X2JpYXMsIGRhdGE9YmVlcl9zdW1tYXJ5KQ0Kc3VtbWFyeShmaXQpDQpgYGANCg0KSXQgYXBwZWFycyB0aGF0IHdlIHNob3VsZCBhZGQgaW4gdGhlIHJldmlld2VycyBiaWFzIGludG8gdGhlIGVzdGltYXRlcyB0byBnZXQgYSBiZXR0ZXIgcHJlZGljdG9yIG9mIHRoZSBhY3R1YWwgcmV2aWV3LiBMZXRzIHNlZSB3aGF0IGhhcHBlbnMgd2hlbiB3ZSBhZGQgdGhhdCB0byBvdXIgcHJlZGljdG9yLg0KYGBge1J9DQpnZXRCZWVyUmF0aW5nIDwtIGZ1bmN0aW9uKHVzZXIsIGJlZXIpew0KICBpZih1c2VyX2JlZXJbdXNlcixiZWVyXSAhPSAwKXsNCiAgICBwYXN0ZSgiUHJldmlvdXNseSBSYXRlZDoiLCB1c2VyX2JlZXJbdXNlcixiZWVyXSkNCiAgfQ0KICBlbHNlew0KICAgIHByZWRpY3RlZCA9IHJvdW5kKGJlZXJQcmVkaWN0W3VzZXIsYmVlcl0gKyANCiAgICAgICAgICAgICAgICAgICAgICAgIGJlZXJfc3VtbWFyeSRyZXZpZXdfYmlhc1tiZWVyX3N1bW1hcnkkcmV2aWV3X3Byb2ZpbGVuYW1lID09IHVzZXJdLCAxKQ0KICAgIHBhc3RlKCJQcmVkaWN0ZWQgUmF0aW5nOiIsIHByZWRpY3RlZCkNCiAgfQ0KfQ0KYGBgDQoNCkxvb2tpbmcgYXQgdGhlIHNhbWUgdXNlciBiZWVyIGNvbWJpbmF0aW9uIGFzIGJlZm9yZSwgd2Ugc2VlIHRoYXQgdGhlIHByZWRpY3RlZCByYXRpbmcgaXMgbG93ZXJlZCBzbGlnaHRseSBmcm9tIHRoZSBiYXNlIHByZWRpY3Rpb24uDQpgYGB7Un0NCmdldEJlZXJSYXRpbmcoIkJlZXJMb3Zlcjk5IiwgIkFsYXNrYW4gU21va2VkIFBvcnRlciIpDQpgYGANCg0KRmluYWxseSBsZXRzIHRha2UgYSBsb29rIGF0IHdoYXQgaGFwcGVucyB0byB0aGUgUk1TRSB3aGVuIHdlIHRha2UgYWxsIG9mIHRoZSBwcmVkaWN0ZWQgdmFsdWVzIGFuZCBhZGQgdGhlIHVzZXIgYmlhcyB0byB0aGUgcHJlZGljdGVkIHZhbHVlcy4gSW50ZXJlc3RpbmdseSBlbm91Z2ggd2Ugc2VlIHRoYXQgdGhlIFJNU0UgaXMgYWN0dWFsbHkgaW5jcmVhc2VkIGFkZGluZyBpbiB0aGUgdXNlciBiaWFzIHRvIHRoZSBwcmVkaWN0aW9ucy4NCmBgYHtSfQ0KYmVlclByZWRpY3QyIDwtIHN3ZWVwKGJlZXJQcmVkaWN0LCAxLCBiZWVyX3N1bW1hcnkkcmV2aWV3X2JpYXMsICIrIikNClJNU0UoYmVlclByZWRpY3QyLCB1c2VyX2JlZXJOQSkNCmBgYA0KDQojIyA2IC0gQ29uY2x1c2lvbg0KVGhpcyBwcm9qZWN0IHJlYWxseSBoZWxwZWQgdXMgdG8gZ2V0IGEgc3Ryb25nZXIgZ3Jhc3Agb24gdGhlIFNpbmd1bGFyIFZhbHVlIERlY29tcG9zaXRpb24gcHJvY2Vzcy4gSXQgd2FzIGludGVyZXN0aW5nIHRvIHNlZSBob3cgdGhpcyBwcm9jZXNzIHdvcmtzIHdoZW4gY29tcGFyZWQgdG8gY29uc3RydWN0aW5nIHRoZSBjb2xsYWJvcmF0aXZlIGZpbHRlcmluZyBtb2RlbHMgdXNpbmcgdGhlICpyZWNvbW1tZW5kZXJsYWIqIHBhY2thZ2UuIEZyb20gd2hhdCB3ZSBoYXZlIGRvbmUgaGVyZSBpdCB3aWxsIGJlIGludGVyZXN0aW5nIHRvIHNlZSB0aGUgdGVjaG5pcXVlcyBmb3IgdXNpbmcgaW5mb3JtYXRpb24gcmVsYXRlZCB0byB0aGUgcmF0aW5ncyB0byBoZWxwIGltcHJvdmUgdGhlIHByZWRpY3RpdmUgcG93ZXIgb2Ygb3VyIHJlY29tbWVuZGVyIHN5c3RlbXMuIHdlIHdlcmUgYSBiaXQgc3VycHJpc2VkIHRvIHNlZSB0aGF0IHRoZSBpbmZvcm1hdGlvbiBnYXRoZXJlZCBmcm9tIHRoZSBsaW5lYXIgcmVncmVzc2lvbiBwcm9jZXNzIGFjdHVhbGx5IG1hZGUgdGhlIHJlY29tbWVuZGF0aW9uIGEgYml0IHdvcnNlLiBXZSB3b3VsZCBhbHNvIGxpa2VkIHRvIGhhdmUgYmVlbiBhYmxlIHRvIGV4cGxvcmUgdGhlIFNWRCBwb3Rpb24gb2YgdGhlICpyZWNvbWVuZGVybGFiKiBwYWNrYWdlIGJ1dCB1bmZvcnR1bmF0ZWx5IHJhbiBvdXQgb2YgdGltZSB0byBleHBsb3JlIHRoaXMuIA==