First LinkedIn Intro, then BonzyBuddy 2.0

Last week, LinkedIn published an indepth technical explanation of how their new LinkedIn Intro mobile product works on iOS. What Intro does is basically display LinkedIn data about your contacts directly in your email client - similar to what Rapportive did for gmail. It’s a cool app but the implementation details LinkedIn shared ignited an Internet firestorm, especially among the startup/hacker crowd.

How Intro works is it basically modifies the users normal iOS email client so that it connects through a LinkedIn proxy server instead of interacting with their webmail provider directly. What this does, is allow LinkedIn to dynamically modify a user’s email before it reaches their mail client, depending on if the user is connected to the sender on LinkedIn. From a IT security standpoint, introducing a third party that would sit between a user and the mail server they’re connecting to undoubtedly introduces a new attack vector but what really caught my interest was how LinkedIn was achieving this. In order to smoothly update the user’s proxy settings, LinkedIn is using a iOS feature known as Configuration Profiles.

I’m not familiar with the iOS SDK or APIs so this was the first time I’d heard about Configuration Profiles. In short, what they allow an app to do is install a set of settings on an iOS device - from email and web proxy settings to additional credentials and SSL keys. Configuration profiles are typically used in enterprise environments to allow a company’s IT department to quickly configure the settings on an employee’s iOS device. When provisioning a new device, IT would basically use the configuration profile to install things like a VPN, internal credentials, etc. So what’s the problem?

Well according to the LinkedIn post and comments from users that have used profiles before, the user experience of installing a profile which radically alters your iOS system settings is surprisingly unassuming. As a user, you click through a couple of prompts and boom, all of a sudden Safari is using a proxy server to fetch websites. So what nefarious things could you do by routing iOS mobile traffic through a proxy server? Unsolicited injected display advertising.

On the desktop web, unscrupulous extension developers have been monetizing their install base by injecting display ads into the browsing experience of their users for years. From companies like Bonzi Buddy to newer companies like PageRage, the model is tried, true, and profitable. However, on mobile there isn’t an obvious opportunity to inject ads and get access to the rapidly growing number of mobile web impressions. It seems like using configuration profiles would be the perfect vector to change this. Crapware iOS developers could quietly prompt their users to install a configuration profile to get access to “hot new features” and then surreptitiously start injecting display ads into websites on the proxy server.

I’m not familiar enough with iOS development to speak to how easy developing an app like this would be or if it would get past the app store approval process, but if it’s feasible someone is certainly going to do it. If anyone is familiar with an app already doing this, I’d love to know about it.

Symfony2: Using FOSUserBundle with multiple EntityManagers

Last week, we were looking to setup one of our Symfony2 projects to use a master/slave MySQL configuration. We’d looked into using the MasterSlaveConnection Doctrine2 connection class, but unfortunately it doesn’t really work the way you’d expect. Anyway, the “next best” way to set up master/slave connections seemed to be creating two separate EntityManagers, one pointing at the master and one at the slave. Setting up the Doctrine configurations for this is pretty straightforward, you’ll end up with YAML that looks like:

# Doctrine Configuration
doctrine:
    dbal:
      connections:
        default:
          driver: %database_driver%
          host: %database_host%
          port: %database_port%
          dbname: %database_name%
          user: %database_user%
          password: %database_password%
          charset: UTF8
        slave:
          driver: %database_driver%
          host: %database_slave_host%
          port: %database_slave_port%
          dbname: %database_slave_name%
          user: %database_slave_user%
          password: %database_slave_password%
          charset: UTF8

    orm:
        auto_generate_proxy_classes: %kernel.debug%
        default_entity_manager: default
        entity_managers:
          default:
            connection: default
            mappings:
              SetfiveBundle:
                dir: Entity
              FOSUserBundle: ~
          slave:
            connection: slave
            mappings:
              SetfiveBundle:
                dir: Entity
              FOSUserBundle: ~

At face value, it looked like everything was working fine but it turns out they weren’t - the FOSUserBundle entities weren’t getting properly setup on the slave connection. Turns out, because FOSUserBundle uses Doctrine2 superclasses to setup it’s fields there’s no way to natively use FOSUserBundle with multiple entity managers. The key issue is that since the UserProvider checks the class of a user being refreshed, you can’t just copy the FOSUserBundle fields directly into your entity:

<?php
// From https://github.com/FriendsOfSymfony/FOSUserBundle/blob/6fc28e41805868c6c635de12266c40595c2e7ce8/Security/UserProvider.php

    public function refreshUser(SecurityUserInterface $user)
    {
        if (!$user instanceof User && !$user instanceof PropelUser) {
            throw new UnsupportedUserException(sprintf('Expected an instance of FOS\UserBundle\Model\User, but got "%s".', get_class($user)));
        }

        if (null === $reloadedUser = $this->userManager->findUserBy(array('id' => $user->getId()))) {
            throw new UsernameNotFoundException(sprintf('User with ID "%d" could not be reloaded.', $user->getId()));
        }

        return $reloadedUser;
    }

So how do you get around this? Turns out, you need to add a custom UserProvider to bypass the instance class check. My UserProvider ended up looking like:

<?php

namespace Setfive\AcmeBundle\Security;

use FOS\UserBundle\Security\UserProvider as BaseUserProvider;
use Symfony\Component\Security\Core\User\UserInterface as SecurityUserInterface;

class UserProvider extends BaseUserProvider {

    /**
* Constructor.
*
* @param UserManagerInterface $userManager
*/
    public function __construct($userManager)
    {
        parent::__construct($userManager);
    }

    public function refreshUser(SecurityUserInterface $user)
    {

        if (null === $reloadedUser = $this->userManager->findUserBy(array('id' => $user->getId()))) {
            throw new UsernameNotFoundException(sprintf('User with ID "%d" could not be reloaded.', $user->getId()));
        }

        return $reloadedUser;
    }

}

And then the additional YAML configurations you need are:

# services.yml

setfive.security.userProvider:
   class: Setfive\AcmeBundle\Security\UserProvider
   arguments: [@fos_user.user_manager] 


# security.yml

providers:
  fos_userbundle:
    id: fos_user.user_provider.username
  setfive_user:
    id: setfive.security.userProvider

The last step is copying all the FOSUserBundle fields directly into your User entity and update it to not extend the FOSUserBundle base class. Anyway, that’s it - two EntityManagers and one FOSUserBundle.

Tech: Three reasons healthcare.gov failed all of us

Since going live early this month, Healthcare.gov has been universally panned for being unstable, unusable, and close to an outright disaster. As the publicity of the problems have intensified, it’s become clear that the project has was mismanaged and probably sloppily developed. The team certainly faced daunting challenges like going from 0 to full capacity overnight but with a reported budget of over $50 million dollars and three years to execute it’s hard to have too much sympathy. Anyway, without knowing the project guidelines, business constraints, and other considerations it would be irresponsible to armchair quarterback and predict technical decisions that would have performed any better. However, on the non-technical side there are some strategic and architectural decisions that are disappointing.

The Team

Like any large project, the success of the project is ultimately driven by the strength of the team behind it. The experience, structure, and mindset of the team building the project will be the key drivers for what tradeoffs are made, how things are architected, and of course how development is executed. Unfortunately for us, according to the Washington Post, CGI Federal the company that built healthcare.gov is both a relative newcomer to US government IT consulting and also has a spotty track record of building large, commercial web applications.

I’ll be the first to admit that I have no background in government procurement, but it strikes me as odd that the Obama administration didn’t handpick the team that was going to build the software for their watershed legislation. Most notably, since the sweeping democratic win in 2008 has largely been attributed to the Obama campaign’s exceptional technology resources. Brad Feld actually suggested bringing in Harper Reed who served as the CTO of the campaign to fix the problems, but why wasn’t he leading the project from the outset?

Transparency (or lack thereof)

For an administration that campaigned on transparency (lets ignore NSA/drones), the lack of transparency surrounding healthcare.gov has been severely dissapointing. The lack of information has left media outlets reporting outlandish claims like that the site is 500 million lines of code or that Verizon has been brought in to help triage the situation. Apart from technical details, the White House has also remained completely silent on visitor and enrollment statistics even though they clearly have them since the site is running a slew of analytics and monitoring services. Leaving the public in the dark has driven rampant speculation and left everyone wondering "What if they aren't releasing data because its THAT bad".

Another area where healthcare.gov has been dissapointingly secretive is the actual source code of the project. Although some people, including Fred Wilson, are recommending open sourcing the code as a potential triage measure the code really should of been available from the outset. Even if the proprietary licensed technology had remained private, tax payer dollars financed the site and open sourcing it from the outset would have added some much needed oversight and accountability. Looking at the site, its using several open source libraries anyway including Twitter Bootstrap, jQuery, BackboneJS, and apparently violating the OSS license for jQuery.Datatables.

No Plan B

One of the accepted truths in software engineering is that shit is going to break and that bugs will crop up in even the most heavily tested applications. Mix in things like legacy integrations and 3rd party APIs and issues almost become a guarantee. Despite this, looking at the rollout of healthcare.gov it's clear that the team had no contingency plans for how to handle various error scenerios. As pieces of the application started to fail, they didn't have any way to mitigate poor user experiences, losing data, or simply showing "fail whale" style error messages. And although bad at launch, the situation still seems just as bad 3 weeks later - with their band aid to simply boot users once they've reached their concurrent session limit.

Building a high traffic web app that interfaces with legacy systems, is highly available, and is also easy to use is undoubtedly still difficult but the failures of healthcare.gov haven’t just been technological but procedural and organizational as well. Hopefully someone has the experience and political clout to right the ship before the website’s troubled launch defines the healthcare law it was built to implement.

PHP: Geocoding with MaxMind and nginx

Earlier this week, one of our adtech clients reached out asking if we could setup IP based geocoding for one of their applications. At a high level, what the application basically does is serve as a backend for an advertising pixel which is embedded on the sites of various publishers. When users are visiting a publisher’s site, the JS pixel makes a HTTP request to our backend from the client’s browser, receives a data payload from the backend, and then does various computations on the frontend.

What our client was looking to do was add the geocoding data into the payload that is returned by the backend. We’ve had some success with the MaxMind database in the past so we decided to investigate using that solution here as well. Initially, we implemented the geocoding using the static MaxMind database along with PHP and memcached to cache the “warmed” MaxMind PHP object. Unfortunately, using PHP presented significant performance issues at the scale we were serving requests. At an average of 20,000 requests/minute, the additional load introduced by the PHP processes serializing and deserializing the MaxMind objects would have ultimately been prohibitively expensive, even across 3 frontends.

So what’s the alternative? Turns out, there’s actually an nginx module that leverages the MaxMind database to make the geocoding data available as CGI parameters. Effectively, this lets you access the geocoding variables for the client’s IP address directly from the $_SERVER variable in PHP. Here’s how you set it up:

Depending on what your setup is, you’ll just need to enable the geoip module in ngnix by following the directions here. Once you have ngnix recompiled with the module on, you’ll need to add the configuration parameters specified into your “server” block. The final configuration step is to add the variables that you want exposed as CGI parameters. All together, you’ll need to end up making these modifications to your config files:

http {

  # .... rest of your configs

  # You can download these files from http://dev.maxmind.com/geoip/legacy/geolite/
  # These files need to be in the same directory as your config file or just use absolute paths
  geoip_country GeoIP.dat;
  geoip_city    GeoLiteCity.dat;

}

server {
  # ...rest of your configs
 
 # make the variables available in PHP 
 fastcgi_param GEO_COUNTRY_CODE $geoip_city_country_code;
 fastcgi_param GEO_CITY $geoip_city;
 fastcgi_param GEO_DMA $geoip_dma_code;
 fastcgi_param GEO_LATITUDE $geoip_latitude;
 fastcgi_param GEO_LONGITUDE $geoip_longitude;
}

Reload or restart ngnix. Then, to access the variables in PHP you can just grab them out of the $_SERVER variable like:

<?php

foreach( array("GEO_COUNTRY_CODE", "GEO_COUNTRY_CITY", "GEO_DMA", "GEO_LATITUDE", "GEO_LONGITUDE") as $key){
  echo $_SERVER[ $key ] . "\n";
}

That’s about it. From our tests, adding the geo module has a negligible effect on performance which is awesome. Of course, your mileage may vary but it’ll certainly be faster than using PHP directly.

PHP: How fast is HipHop PHP?

In the last post, we walked through how to install HipHop PHP on a Ubuntu 13.04 EC2. Well thats great but it now leads us to the question of how fast HipHop PHP actually is. The problem with “toy benchmarks” is they tend to not really capture the real performance characteristics of whatever you’re benchmarking. This is why comparing the performance of a “Hello World” app across various languages and frameworks is generally a waste of time, since its not capturing a real world scenario. Luckily, I actually have some “real world”’ish benchmarks from my PHP: Does “big-o” complexity really matter? post a couple of months ago.

Ok so great, lets checkout the repository, run the benchmark with HipHop and Zend PHP, and then marvel at how HipHop blows Zend PHP out of the water.

$ git clone git@github.com:Setfive/setfive.github.com.git
$ cd setfive.github.com/autocomplete_test/
$ ~/dev/hiphop-php/hphp/hhvm/hhvm --hphp  -thhbc -o some_dir autocompleteBenchmark.php algorithms.php
$ ~/dev/hiphop-php/hphp/hhvm/hhvm -vRepo.Authoritative=true -vRepo.Central.Path=some_dir/hhvm.hhbc -vEval.Jit=1 autocompleteBenchmark.php
test_name, min, max, average
readMemoryScan,9.4527468681335, 9.7483551502228, 9.6144312620163
unsortedTableScan,9.2616581916809, 9.5636370182037, 9.3831548213959
sortedTableScan,4.0028560161591, 4.2003200054169, 4.1263886928558
serializednFileScan,0.12966299057007, 0.1957049369812, 0.14499731063843

$ php autocompleteBenchmark.php
test_name, min, max, average
readMemoryScan,1.1715579032898, 1.3546500205994, 1.2789875268936
unsortedTableScan,0.98337507247925, 1.0579788684845, 1.0203387260437
sortedTableScan,0.41418695449829, 0.54504704475403, 0.47947511672974
serializednFileScan,0.25355696678162, 0.31970500946045, 0.27470426559448

Wtf?

Well so that is weird, in 3 out of the 4 tests HipHop is an order of magnitude slower than Zend PHP. Clearly, something is definitely not right. I double checked the commands and everything is being run correctly. I started debugging the readMemoryScan function on HipHop specifically and it turns out that the problem function is actually str_getcsv. I decided to remove that function as well as the array_maps() since I wasn’t sure if HipHop would be able to optimize given the anonymous function being passed in. The new algorithms file is algorithms_hiphop.php which has str_getcsv replaced with an explode and array_map replaced with a loop.

Running the same benchmarks again except with the new algorithms file gives you:

$ ~/dev/hiphop-php/hphp/hhvm/hhvm -vRepo.Authoritative=true -vRepo.Central.Path=some_dir/hhvm.hhbc -vEval.Jit=1 autocompleteBenchmark.php
test_name, min, max, average
readMemoryScan,0.35798001289368, 0.43651509284973, 0.37085726261139
unsortedTableScan,0.25601601600647, 0.31864595413208, 0.27621953487396
sortedTableScan,0.12703394889832, 0.18750810623169, 0.13350143432617
serializednFileScan,0.13122606277466, 0.19621300697327, 0.15197308063507

$ php autocompleteBenchmark.php
test_name, min, max, average
readMemoryScan,0.52727389335632, 0.70799994468689, 0.64662590026855
unsortedTableScan,0.41211080551147, 0.48418807983398, 0.46322596073151
sortedTableScan,0.20118999481201, 0.2757511138916, 0.22925112247467
serializednFileScan,0.2424430847168, 0.36282706260681, 0.30896728038788

Wow. So the HipHop implementation is clearly faster but what’s even more surprising is that the Zend PHP implementation gains a significant speedup just by removing str_getcsv and array_map.

Anyway, as expected, HipHop is a faster implementation most likely due to its JIT compilation and additional optimizations that it’s able to add along the way.

Despite the speedup though, Facebook has made it clear that HipHop will only support a subset of the PHP language, notably that the dynamic features will never be implemented. At the end of the day, its not clear if HipHop will gain any mainstream penetration but hopefully it’ll push Zend to keep improving their interpreter and potentially incorporate some of HipHop’s JIT features.

Per Dan’s comment below, HHVM currently supports almost all of PHP 5.5s features.

Why is str_getcsv so slow?

Well benchmarks are all fine and well but I was curious why str_getcsv was so slow on both Zend and HipHop. Digging around, the HipHop implementation looks like:

<?php

function str_getcsv(
  $input,
  $delimiter = ',',
  $enclosure = '"',
  $escape = '\\',
) {
  $args = array($delimiter, $enclosure, $escape);
  $defaults = array(',', '"', '\\');

  $args_len = count($args);
  for ($i = 0; $i < $args_len; ++$i) {
    $arg_len = strlen($args[$i]);

    // fgetcsv returns false if passed anything but a single char for its
    // last three args. str_getcsv's behavior is to truncate strings down
    // to their first char, and to turn invalid args into the default args.
    if ($arg_len === 0) {
      $args[$i] = $defaults[$i];
    } else if ($arg_len > 1) {
      $args[$i] = substr($args[$i], 0, 1);
    }
  }

  $temp = tmpfile();
  fwrite($temp, $input);
  fseek($temp, 0);
  $ret = fgetcsv($temp, 0, $args[0], $args[1], $args[2]);
  fclose($temp);
  return $ret !== false ? $ret : array(null);
}

So basically just a wrapper around fgetcsv that works by writing the string to a temporary file. I’d expect file operations to be slow but I’m still surprised they’re that slow.

Anyway, looking at the Zend implementation it’s a native C function that calls into php_fgetcsv but doesn’t use temporary files.

/* {{{ proto array str_getcsv(string input[, string delimiter[, string enclosure[, string escape]]])
Parse a CSV string into an array */
PHP_FUNCTION(str_getcsv)
{
	char *str, delim = ',', enc = '"', esc = '\\';
	char *delim_str = NULL, *enc_str = NULL, *esc_str = NULL;
	int str_len = 0, delim_len = 0, enc_len = 0, esc_len = 0;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|sss", &str, &str_len, &delim_str, &delim_len, 
		&enc_str, &enc_len, &esc_str, &esc_len) == FAILURE) {
		return;
	}
	
	delim = delim_len ? delim_str[0] : delim;
	enc = enc_len ? enc_str[0] : enc;
	esc = esc_len ? esc_str[0] : esc;

	php_fgetcsv(NULL, delim, enc, esc, str_len, str, return_value TSRMLS_CC);
}
/* }}} */

PHPAPI void php_fgetcsv(php_stream *stream, char delimiter, char enclosure, char escape_char, size_t buf_len, char *buf, zval *return_value TSRMLS_DC) /* {{{ */
{
	char *temp, *tptr, *bptr, *line_end, *limit;
	size_t temp_len, line_end_len;
	int inc_len;
	zend_bool first_field = 1;

	/* initialize internal state */
	php_mblen(NULL, 0);

	/* Now into new section that parses buf for delimiter/enclosure fields */

	/* Strip trailing space from buf, saving end of line in case required for enclosure field */

	bptr = buf;
	tptr = (char *)php_fgetcsv_lookup_trailing_spaces(buf, buf_len, delimiter TSRMLS_CC);
	line_end_len = buf_len - (size_t)(tptr - buf);
	line_end = limit = tptr;

	/* reserve workspace for building each individual field */
	temp_len = buf_len;
	temp = emalloc(temp_len + line_end_len + 1);

	/* Initialize return array */
	array_init(return_value);

	/* Main loop to read CSV fields */
	/* NB this routine will return a single null entry for a blank line */

	do {
		char *comp_end, *hunk_begin;

		tptr = temp;
		inc_len = (bptr < limit ? (*bptr == '\0' ? 1: php_mblen(bptr, limit - bptr)): 0);
		if (inc_len == 1) {
			char *tmp = bptr;
			while (isspace((int)*(unsigned char *)tmp)) {
				tmp++;
			}
			if (*tmp == enclosure) {
				bptr = tmp;
			}
		}

		if (first_field && bptr == line_end) {
			add_next_index_null(return_value);
			break;
		}
		first_field = 0;
		/* 2. Read field, leaving bptr pointing at start of next field */
		if (inc_len != 0 && *bptr == enclosure) {
			int state = 0;

			bptr++;	/* move on to first character in field */
			hunk_begin = bptr;

			/* 2A. handle enclosure delimited field */
			for (;;) {
				switch (inc_len) {
					case 0:
						switch (state) {
							case 2:
								memcpy(tptr, hunk_begin, bptr - hunk_begin - 1);
								tptr += (bptr - hunk_begin - 1);
								hunk_begin = bptr;
								goto quit_loop_2;

							case 1:
								memcpy(tptr, hunk_begin, bptr - hunk_begin);
								tptr += (bptr - hunk_begin);
								hunk_begin = bptr;
								/* break is omitted intentionally */

							case 0: {
								char *new_buf;
								size_t new_len;
								char *new_temp;

								if (hunk_begin != line_end) {
									memcpy(tptr, hunk_begin, bptr - hunk_begin);
									tptr += (bptr - hunk_begin);
									hunk_begin = bptr;
								}

								/* add the embedded line end to the field */
								memcpy(tptr, line_end, line_end_len);
								tptr += line_end_len;

								if (stream == NULL) {
									goto quit_loop_2;
								} else if ((new_buf = php_stream_get_line(stream, NULL, 0, &new_len)) == NULL) {
									/* we've got an unterminated enclosure,
									 * assign all the data from the start of
									 * the enclosure to end of data to the
									 * last element */
									if ((size_t)temp_len > (size_t)(limit - buf)) {
										goto quit_loop_2;
									}
									zval_dtor(return_value);
									RETVAL_FALSE;
									goto out;
								}
								temp_len += new_len;
								new_temp = erealloc(temp, temp_len);
								tptr = new_temp + (size_t)(tptr - temp);
								temp = new_temp;

								efree(buf);
								buf_len = new_len;
								bptr = buf = new_buf;
								hunk_begin = buf;

								line_end = limit = (char *)php_fgetcsv_lookup_trailing_spaces(buf, buf_len, delimiter TSRMLS_CC);
								line_end_len = buf_len - (size_t)(limit - buf);

								state = 0;
							} break;
						}
						break;

					case -2:
					case -1:
						php_mblen(NULL, 0);
						/* break is omitted intentionally */
					case 1:
						/* we need to determine if the enclosure is
						 * 'real' or is it escaped */
						switch (state) {
							case 1: /* escaped */
								bptr++;
								state = 0;
								break;
							case 2: /* embedded enclosure ? let's check it */
								if (*bptr != enclosure) {
									/* real enclosure */
									memcpy(tptr, hunk_begin, bptr - hunk_begin - 1);
									tptr += (bptr - hunk_begin - 1);
									hunk_begin = bptr;
									goto quit_loop_2;
								}
								memcpy(tptr, hunk_begin, bptr - hunk_begin);
								tptr += (bptr - hunk_begin);
								bptr++;
								hunk_begin = bptr;
								state = 0;
								break;
							default:
								if (*bptr == enclosure) {
									state = 2;
								} else if (*bptr == escape_char) {
									state = 1;
								}
								bptr++;
								break;
						}
						break;

					default:
						switch (state) {
							case 2:
								/* real enclosure */
								memcpy(tptr, hunk_begin, bptr - hunk_begin - 1);
								tptr += (bptr - hunk_begin - 1);
								hunk_begin = bptr;
								goto quit_loop_2;
							case 1:
								bptr += inc_len;
								memcpy(tptr, hunk_begin, bptr - hunk_begin);
								tptr += (bptr - hunk_begin);
								hunk_begin = bptr;
								break;
							default:
								bptr += inc_len;
								break;
						}
						break;
				}
				inc_len = (bptr < limit ? (*bptr == '\0' ? 1: php_mblen(bptr, limit - bptr)): 0);
			}

		quit_loop_2:
			/* look up for a delimiter */
			for (;;) {
				switch (inc_len) {
					case 0:
						goto quit_loop_3;

					case -2:
					case -1:
						inc_len = 1;
						php_mblen(NULL, 0);
						/* break is omitted intentionally */
					case 1:
						if (*bptr == delimiter) {
							goto quit_loop_3;
						}
						break;
					default:
						break;
				}
				bptr += inc_len;
				inc_len = (bptr < limit ? (*bptr == '\0' ? 1: php_mblen(bptr, limit - bptr)): 0);
			}

		quit_loop_3:
			memcpy(tptr, hunk_begin, bptr - hunk_begin);
			tptr += (bptr - hunk_begin);
			bptr += inc_len;
			comp_end = tptr;
		} else {
			/* 2B. Handle non-enclosure field */

			hunk_begin = bptr;

			for (;;) {
				switch (inc_len) {
					case 0:
						goto quit_loop_4;
					case -2:
					case -1:
						inc_len = 1;
						php_mblen(NULL, 0);
						/* break is omitted intentionally */
					case 1:
						if (*bptr == delimiter) {
							goto quit_loop_4;
						}
						break;
					default:
						break;
				}
				bptr += inc_len;
				inc_len = (bptr < limit ? (*bptr == '\0' ? 1: php_mblen(bptr, limit - bptr)): 0);
			}
		quit_loop_4:
			memcpy(tptr, hunk_begin, bptr - hunk_begin);
			tptr += (bptr - hunk_begin);

			comp_end = (char *)php_fgetcsv_lookup_trailing_spaces(temp, tptr - temp, delimiter TSRMLS_CC);
			if (*bptr == delimiter) {
				bptr++;
			}
		}

		/* 3. Now pass our field back to php */
		*comp_end = '\0';
		add_next_index_stringl(return_value, temp, comp_end - temp, 1);
	} while (inc_len > 0);

out:
	efree(temp);
	if (stream) {
		efree(buf);
	}
}
/* }}} */

Looking at the actual implementation of php_fgetcsv though its not surprising its significantly slower compared to explode().