3
\$\begingroup\$

I am working on an API to return all the possible flight route between point A and B.

I have a set of data which includes the flight number, airline, departure airport, destination airport, departure time, and arrival time.

Keep in mind I am also catering connecting flights. But the connections can only happen between hubs.

I have 5 Hubs and 5 Airlines. Each hub is the base for an airline e.g. Dubai for Emirates, Istanbul for Turkish.

The API needs to calculate all possible routes based on all the flights of these 5 airlines.

The sample result could be (assuming Istanbul, London & Nairobi are hubs):

User wants to go from Dubai to New York

  • Route 1: Dubai to New York (EK203)
  • Route 2: Connecting Dubai to Istanbul to New York (EK123 , TK3)
  • Route 3: Dubai to Istanbul to London to Nairobi to New York

Currently this system is build on SQL DB and .NET API. We are able to do all above, and it works fine for direct flights but it takes up to 30 seconds when the connections are more than 2.

The data set we are running this on includes all the flights for all 5 airlines so the data set is huge.

I was suggested to use GraphQL.

I am looking for suggestions on how to bring this to under 2 seconds of response.

Below is a chunk of SQL Query:

WITH RoutesCTE AS
( 
select 
( dgl_city . Title + ', ' + dgl_country . Title + '|' + agl_city . Title + ', ' + agl_country . Title ) as [Route] 
, 0 as TransfersCount 
 
, CAST (( '[{' + 
 
'"RosterID":"' + CAST ( ISNULL ( rtt . RosterID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"FlightNumber":"' + CAST ( ISNULL ( rtt . FlightNumber , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ReoccuranceCode":"' + ISNULL ( rtt . Code , '' ) + '",' + 
'"TravelerAspNetUserID":"' + CAST ( ISNULL ( rtt . TravelerAspNetUserID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"TravelerUserCode":"' + ISNULL ( t_asp . Code , '' ) + '",' + 
 
'"SortOrder":"' + Cast (( 0 ) AS NVARCHAR ( MAX )) + '",' + 
 
'"DepartureAirportIATA":"' + ISNULL ( rtt . DepartureAirportIATA , '' ) + '",' + 
'"DepartureAirportTitle":"' + ISNULL ( da . Title , '' ) + '",' + 
 
'"DepartureGeoLocationCityID":"' + CAST ( ISNULL ( dgl_city . GeoLocationID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityTitle":"' + CAST ( ISNULL ( dgl_city . Title , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityIsHub":"' + CAST ( ISNULL ( dgl_city . IsHub , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityIsExceptionEnabledAtPickup":"' + CAST ( ISNULL ( dgl_city . IsExceptionEnabledAtPickup , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityIsExceptionEnabledAtDropoff":"' + CAST ( ISNULL ( dgl_city . IsExceptionEnabledAtDropoff , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityISOCode":"' + CAST ( ISNULL ( dgl_city . ISOCode , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCountryID":"' + CAST ( ISNULL ( dgl_country . GeoLocationID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCountryTitle":"' + CAST ( ISNULL ( dgl_country . Title , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCountryISOCode":"' + CAST ( ISNULL ( dgl_country . ISOCode , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureDateTime":"' + CAST ( ISNULL ( rtt . DepartureDateTime , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureAirportTerminal":"' + CAST ( ISNULL ( rtt . DepartureTerminal , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureAirportGate":"' + ISNULL ( rtt . DepartureGate , '' ) + '",' + 
 
'"ArrivalAirportIATA":"' + ISNULL ( rtt . ArrivalAirportIATA , '' ) + '",' + 
'"ArrivalAirportTitle":"' + ISNULL ( aa . Title , '' ) + '",' + 
 
'"ArrivalGeoLocationCityID":"' + CAST ( ISNULL ( agl_city . GeoLocationID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCityTitle":"' + ISNULL ( agl_city . Title , '' ) + '",' + 
'"ArrivalGeoLocationCityIsHub":"' + CAST ( ISNULL ( agl_city . IsHub , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCityIsExceptionEnabledAtPickup":"' + CAST ( ISNULL ( agl_city . IsExceptionEnabledAtPickup , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCityIsExceptionEnabledAtDropoff":"' + CAST ( ISNULL ( agl_city . IsExceptionEnabledAtDropoff , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCityISOCode":"' + CAST ( ISNULL ( agl_city . ISOCOde , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCountryID":"' + CAST ( ISNULL ( agl_country . GeoLocationID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCountryTitle":"' + ISNULL ( agl_country . Title , '' ) + '",' + 
'"ArrivalGeoLocationCountryISOCode":"' + ISNULL ( agl_country . ISOCode , '' ) + '",' + 
'"ArrivalDateTime":"' + CAST ( ISNULL ( rtt . ArrivalDateTime , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalAirportTerminal":"' + ISNULL ( rtt . ArrivalTerminal , '' ) + '",' + 
'"ArrivalAirportGate":"' + ISNULL ( rtt . ArrivalGate , '' ) + '",' + 
 
'"AirlineIATA":"' + ISNULL ( rtt . AirlineIATA , '' ) + '",' + 
'"AirlineTitle":"' + ISNULL ( air . Title , '' ) + '"' + 
 
'}' + ( case 
when agl_city . GeoLocationID = @ArrivalGeoLocationID then ']' 
WHEN rtt . ArrivalAirportIATA IN ( SELECT a . IATA FROM @ArrivalAirportExceptions a ) THEN ']' 
else '' 
end ) 
) AS NVARCHAR ( MAX )) as JsonObj , 
 
rtt . DepartureAirportIATA , 
rtt . ArrivalAirportIATA , 
rtt . DepartureDateTime , 
rtt . ArrivalDateTime , 
 
da . OriginGeoLocationID as DepartureGeoLocationID , 
dgl_city . Title as DepartureCityTitle , 
dgl_city . UTCOffset as DepartureCityUTCOffset , 
dgl_city . IsHub as DepartureCityIsHub , 
dgl_city . ISOCode as DepartureCityISOCode , 
dgl_city . IsExceptionEnabledAtPickup as DepartureCityIsExceptionEnabledAtPickup , 
dgl_city . IsExceptionEnabledAtDropoff as DepartureCityIsExceptionEnabledAtDropoff , 
dgl_city . IsDSTEnabled as DepartureCityIsDSTEnabled , 
dgl_country . Title as DepartureCountryTitle , 
dgl_country . ISOCOde as DepartureCountryISOCode , 
dgl_country . VAT as DepartureCountryVATInPercentage , 
dgl_country . NFSTaxRegisteration as DepartureCountryNFSTaxRegisteration , 
aa . OriginGeoLocationID as ArrivalGeoLocationID , 
agl_city . Title as ArrivalCityTitle , 
agl_city . IsHub as ArrivalCityIsHub , 
agl_city . ISOCode as ArrivalCityISOCOde , 
agl_city . IsExceptionEnabledAtPickup as ArrivalCityIsExceptionEnabledAtPickup , 
agl_city . IsExceptionEnabledAtDropoff as ArrivalCityIsExceptionEnabledAtDropoff , 
agl_city . IsDSTEnabled as ArrivalCityIsDSTEnabled , 
agl_city . UTCOffset as ArrivalCityUTCOffset , 
agl_country . Title as ArrivalCountryTitle , 
agl_country . ISOCode as ArrivalCountryISOCode , 
agl_country . VAT as ArrivalCountryVATInPercentage , 
agl_country . NFSTaxRegisteration as ArrivalCountryNFSTaxRegisteration , 
rtt . TravelerAspNetUserID 
, rtt . AirlineIATA 
 
FROM @RosterTempTable rtt 
 
INNER JOIN dbo . Airlines air 
ON rtt . AirlineIATA = air . IATA 
 
INNER JOIN dbo . Airports da 
ON rtt . DepartureAirportIATA = da . IATA 
INNER JOIN dbo . GeoLocations dgl_city 
ON da . OriginGeoLocationID = dgl_city . GeoLocationID 
INNER JOIN dbo . GeoLocations dgl_country 
ON dgl_city . GeoLocationCountryID = dgl_country . GeoLocationID 
 
INNER JOIN dbo . Airports aa 
ON rtt . ArrivalAirportIATA = aa . IATA 
INNER JOIN dbo . GeoLocations agl_city 
ON aa . OriginGeoLocationID = agl_city . GeoLocationID 
INNER JOIN dbo . GeoLocations agl_country 
ON agl_city . GeoLocationCountryID = agl_country . GeoLocationID 
 
INNER JOIN dbo . AspNetUsers t_asp 
ON rtt . TravelerAspNetUserID = t_asp . Id 
 
INNER JOIN dbo . Tiers tier 
ON t_asp . TierID = tier . TierID 
AND ( t_asp . KycApproval IS NOT NULL AND t_asp . KycApproval = 'true' ) 
AND tier . IsBarded = 'false' 
 
UNION ALL 
 
SELECT 
 
r . [Route] + '|' + ( r1_agl_city . Title + ', ' + r1_agl_country . Title ) 
, TransfersCount + 1 
, r . JsonObj + CAST (( ',{' + 
 
'"RosterID":"' + CAST ( ISNULL ( r1 . RosterID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"FlightNumber":"' + CAST ( ISNULL ( r1 . FlightNumber , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ReoccuranceCode":"' + ISNULL ( r1 . Code , '' ) + '",' + 
'"TravelerAspNetUserID":"' + CAST ( ISNULL ( r1 . TravelerAspNetUserID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"TravelerUserCode":"' + ISNULL ( t_asp . Code , '' ) + '",' + 
 
'"DepartureAirportIATA":"' + ISNULL ( r1 . DepartureAirportIATA , '' ) + '",' + 
'"DepartureAirportTitle":"' + ISNULL ( r1_da . Title , '' ) + '",' + 
 
'"SortOrder":"' + Cast (( TransfersCount + 1 ) AS NVARCHAR ( MAX )) + '",' + 
 
'"DepartureGeoLocationCityID":"' + CAST ( ISNULL ( r1_dgl_city . GeoLocationID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityTitle":"' + CAST ( ISNULL ( r1_dgl_city . Title , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityIsHub":"' + CAST ( ISNULL ( r1_dgl_city . IsHub , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityIsExceptionEnabledAtPickup":"' + CAST ( ISNULL ( r1_dgl_city . IsExceptionEnabledAtPickup , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityIsExceptionEnabledAtDropoff":"' + CAST ( ISNULL ( r1_dgl_city . IsExceptionEnabledAtDropoff , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCityISOCode":"' + CAST ( ISNULL ( r1_dgl_city . ISOCode , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCountryID":"' + CAST ( ISNULL ( r1_dgl_country . GeoLocationID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCountryTitle":"' + CAST ( ISNULL ( r1_dgl_country . Title , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureGeoLocationCountryISOCode":"' + CAST ( ISNULL ( r1_dgl_country . ISOCode , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureDateTime":"' + CAST ( ISNULL ( r1 . DepartureDateTime , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureAirportTerminal":"' + CAST ( ISNULL ( r1 . DepartureTerminal , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"DepartureAirportGate":"' + ISNULL ( r1 . DepartureGate , '' ) + '",' + 
 
'"ArrivalAirportIATA":"' + ISNULL ( r1 . ArrivalAirportIATA , '' ) + '",' + 
'"ArrivalAirportTitle":"' + ISNULL ( r1_aa . Title , '' ) + '",' + 
 
'"ArrivalCityID":"' + CAST ( ISNULL ( r1_agl_city . GeoLocationID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCityTitle":"' + ISNULL ( r1_agl_city . Title , '' ) + '",' + 
'"ArrivalGeoLocationCityIsHub":"' + CAST ( ISNULL ( r1_agl_city . IsHub , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCityIsExceptionEnabledAtPickup":"' + CAST ( ISNULL ( r1_agl_city . IsExceptionEnabledAtPickup , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCityIsExceptionEnabledAtDropoff":"' + CAST ( ISNULL ( r1_agl_city . IsExceptionEnabledAtDropoff , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCityISOCode":"' + CAST ( ISNULL ( r1_agl_city . ISOCode , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCountryID":"' + CAST ( ISNULL ( r1_agl_country . GeoLocationID , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalGeoLocationCountryTitle":"' + ISNULL ( r1_agl_country . Title , '' ) + '",' + 
'"ArrivalGeoLocationCountryISOCode":"' + ISNULL ( r1_agl_country . ISOCode , '' ) + '",' + 
'"ArrivalDateTime":"' + CAST ( ISNULL ( r1 . ArrivalDateTime , '' ) AS NVARCHAR ( MAX )) + '",' + 
'"ArrivalAirportTerminal":"' + ISNULL ( r1 . ArrivalTerminal , '' ) + '",' + 
'"ArrivalAirportGate":"' + ISNULL ( r1 . ArrivalGate , '' ) + '",' + 
 
'"AirlineIATA":"' + ISNULL ( r1 . AirlineIATA , '' ) + '",' + 
'"AirlineTitle":"' + ISNULL ( air . Title , '' ) + '"' + 
 
'}' + ( case 
when r1_agl_city . GeoLocationID = @ArrivalGeoLocationID then ']' 
WHEN r1 . ArrivalAirportIATA IN ( SELECT a . IATA FROM @ArrivalAirportExceptions a ) THEN ']' 
else '' 
end ) 
) AS NVARCHAR ( MAX )), 
 
r . DepartureAirportIATA , 
r1 . ArrivalAirportIATA , 
r . DepartureDateTime , 
r1 . ArrivalDateTime , 
 
r . DepartureGeoLocationID , 
r . DepartureCityTitle , 
r . DepartureCityUTCOffset , 
r . DepartureCityIsHub , 
r . DepartureCityISOCode , 
r . DepartureCityIsExceptionEnabledAtPickup , 
r . DepartureCityIsExceptionEnabledAtDropoff , 
r . DepartureCityIsDSTEnabled , 
r . DepartureCountryTitle , 
r . DepartureCountryISOCode , 
r . DepartureCountryVATInPercentage , 
r . DepartureCountryNFSTaxRegisteration , 
r1_aa . OriginGeoLocationID as ArrivalGeoLocationID , 
r1_agl_city . Title as ArrivalCityTitle , 
r1_agl_City . IsHub AS ArrivalCityIsHub , 
r1_agl_City . ISOCode AS ArrivalCityISOCode , 
r1_agl_City . IsExceptionEnabledAtPickup AS ArrivalCityIsExceptionEnabledAtPickup , 
r1_agl_City . IsExceptionEnabledAtDropoff AS ArrivalCityIsExceptionEnabledAtDropoff , 
r1_agl_City . IsDSTEnabled AS ArrivalCityIsDSTEnabled , 
r1_agl_City . UTCOffset AS ArrivalCityUTCOffset , 
r1_agl_country . Title as ArrivalCountryTitle , 
r1_agl_country . ISOCode as ArrivalCountryISOCode , 
r1_agl_country . VAT as ArrivalCountryVATInPercentage , 
r1_agl_country . NFSTaxRegisteration as ArrivalCountryNFSTaxRegisteration , 
r1 . TravelerAspNetUserID , 
r1 . AirlineIATA 
 
FROM RoutesCTE r 
 
JOIN @RosterTempTable r1 
 
INNER JOIN dbo . Airlines air 
ON r1 . AirlineIATA = air . IATA 
 
INNER JOIN dbo . Airports r1_da 
ON r1 . DepartureAirportIATA = r1_da . IATA 
INNER JOIN dbo . GeoLocations r1_dgl_city 
ON r1_da . OriginGeoLocationID = r1_dgl_city . GeoLocationID 
INNER JOIN dbo . GeoLocations r1_dgl_country 
ON r1_dgl_city . GeoLocationCountryID = r1_dgl_country . GeoLocationID 
 
INNER JOIN dbo . Airports r1_aa 
ON r1 . ArrivalAirportIATA = r1_aa . IATA 
INNER JOIN dbo . GeoLocations r1_agl_city 
ON r1_aa . OriginGeoLocationID = r1_agl_city . GeoLocationID 
INNER JOIN dbo . GeoLocations r1_agl_country 
ON r1_agl_city . GeoLocationCountryID = r1_agl_country . GeoLocationID 
 
INNER JOIN dbo . AspNetUsers t_asp 
ON r1 . TravelerAspNetUserID = t_asp . Id 
 
INNER JOIN dbo . Tiers tier 
ON t_asp . TierID = tier . TierID 
 
--ON (r.ArrivalAirportIATA = r1.DepartureAirportIATA OR (r.ArrivalCityTitle + ', ' + r.ArrivalCountryTitle) = (r1_dgl_city.Title + ', ' + r1_dgl_country.Title)) 
--ON (((r.AirlineIATA IN ('ey', 'ek') AND r1.AirlineIATA IN ('ey', 'ek')) AND ((r.ArrivalAirportIATA <> r1.DepartureAirportIATA) OR (r.ArrivalAirportIATA <> r1.DepartureAirportIATA))) OR (r.ArrivalAirportIATA = r1.DepartureAirportIATA)) 
ON (( r . ArrivalAirportIATA <> r1 . DepartureAirportIATA ) OR ( r . ArrivalAirportIATA = r1 . DepartureAirportIATA )) 
 
AND r1 . ArrivalAirportIATA <> r . DepartureAirportIATA 
AND (( DATEDIFF ( HOUR , r . ArrivalDateTime , r1 . DepartureDateTime ) > 0 ) AND ( DATEDIFF ( HOUR , r . ArrivalDateTime , r1 . DepartureDateTime ) >= ( @PickupCuttoffTimeInHours + @DeliveryCuttoffTimeInHours + @HandlerCuttoffTimeInHours )) AND ( DATEDIFF ( DAY , r . ArrivalDateTime , r1 . DepartureDateTime ) <= 5 )) 
 
AND PATINDEX ( '%' + ( r1_agl_city . Title + ', ' + r1_agl_country . Title ) + '%' , r . [Route] ) = 0 
 
AND ( r . ArrivalCityIsHub = 'true' OR r . TravelerAspNetUserID = r1 . TravelerAspNetUserID ) 
AND t_asp . KycApproval = 'true' 
AND tier . IsBarded = 'false' 
) 
SELECT 
TOP 500 
[Route] , 
 
TransfersCount AS TotalNumberOfConnectingFlights 
 
, JsonObj , DepartureDateTime , ArrivalDateTime , DATEDIFF ( MILLISECOND , DepartureDateTime , ArrivalDateTime ) as CalculatedTime , 
DepartureGeoLocationID , 
DepartureCityTitle , 
DepartureCityUTCOffset , 
DepartureCityIsHub , 
DepartureCityIsExceptionEnabledAtPickup , 
DepartureCityIsExceptionEnabledAtDropoff , 
DepartureCityIsDSTEnabled , 
DepartureCountryVATInPercentage , 
DepartureCountryNFSTaxRegisteration , 
DepartureCityISOCode , 
DepartureCountryTitle , 
DepartureCountryISOCode , 
ArrivalGeoLocationID , 
ArrivalCityTitle , 
ArrivalCityUTCOffset , 
ArrivalCityIsHub , 
ArrivalCityIsExceptionEnabledAtPickup , 
ArrivalCityIsExceptionEnabledAtDropoff , 
ArrivalCityISOCode , 
ArrivalCountryTitle , 
ArrivalCountryISOCode , 
ArrivalCityIsDSTEnabled , 
ArrivalCountryVATInPercentage , 
ArrivalCountryNFSTaxRegisteration 
 
FROM RoutesCTE 
WHERE TransfersCount <= 4 
AND ( DepartureGeoLocationID = @DepartureGeoLocationID AND ArrivalGeoLocationID = @ArrivalGeoLocationID ) 
ORDER BY DepartureDateTime ASC , DATEDIFF ( MILLISECOND , DepartureDateTime , ArrivalDateTime ) ASC 

\$\endgroup\$
3
\$\begingroup\$

Without same table and data scripts it is very hard to identify performance issues. However, here are a few observations that might help:

  1. Unless I'm reading this wrong, this seems pointless, just adding overhead:

    ON ((r.ArrivalAirportIATA <> r1.DepartureAirportIATA)
        OR (r.ArrivalAirportIATA = r1.DepartureAirportIATA))
    
  2. When using expressions, try to always leave one indexed field unmodified. This allows the optimizer to leverage indexes as much as possible. For example, change:

    (DATEDIFF(HOUR, r.ArrivalDateTime, r1.DepartureDateTime) > 0)
    

    to

    (r1.DepartureDatetime > DATEADD(HOUR,1,r.ArrivalDateTime)
    

    That will allow an index on the DepartureDateTime to be used.
    On a separate note, I think you have a small logic bug here. DATEDIFF(HOUR... compares the specific time component referenced. If comparing 10:59 AM & 11:01 AM, the difference is 1 hour unit. I assume you're actually looking for at least 1 hour between the arrival and next departure. My recommendation will cover this as well, otherwise switch to DATEDIFF(MINUTE... > 60

  3. Using a MAX datatype can slow things down, sometimes significantly. I'd recommend avoiding until it is really needed. On a related note, try using CONCAT('"RosterID":"',r1.RosterID,'",') to simplify reading your code. That will also automatically handle null as empty string.

  4. Recursive CTEs can be problematic for performance, especially when the recursion has unnecessary stuff in it. If at all possible, build your JSON after you've finished the recursion, once you've identified the routes that meet your criteria.

\$\endgroup\$
  • \$\begingroup\$ Appreciate your detailed answer , we decided to take a completely different approach. But appreciate you taking the time. \$\endgroup\$ – Bajwa Jul 17 at 8:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.